How to copy text in markdown format from marks

Looked around the forum, online and in source, but couldn’t find the solution yet. Wondering how to copy-paste a mark from ProseMirror to notepad (plain text), but keep markdown styling.

For example, I’d like to copy this in ProseMirror: test bold test

Then paste it in Notepad, and get this: test **bold** test

I’m looking for bold as the easiest example, but would like to do similar thing for links, annotations (custom mark) etc.

The general idea would be to register DOM event handlers for “copy” (and maybe “cut”), and in those, take the selected content and run that through a MarkdownSerializer. That only takes a node, so you may need to wrap the slice’s fragment property in a node (I think an arbitrary node will do) for it to work.

Thank you! I appreciate the super fast and helpful response, feeling excited to try out the suggestion! While looking at event handlers I noticed clipboardTextParser - would that be better to use instead of DOM copy/cut events?

I’m trying it out now. Found a related issue where I cannot copy a block element that is first in list, will open a separate discuss topic.

clipboardTextParser goes the other way. But yeah, clipboardTextSerializer does what you need here in a simpler way then what I was going on about before.

1 Like

There’s a good example of how to use the clipboardTextSerializer here https://github.com/ueberdosis/tiptap/blob/main/packages/core/src/extensions/clipboardTextSerializer.ts, https://github.com/ueberdosis/tiptap/commit/fe6a3e7491f6a42123d3d8a92ab588f2a40d7799, Replace Image With Text On Text Copy - #7 by jorisgu.

2 Likes

Just for information in case someone else looks for the same behavior. I ended up using a slightly modified version of prosemirror-markdown/to_markdown.js.

I made some changes to my custom-flavor markdown (github + extensions)

  1. Serialize paragraphs as single line
  2. Serialize bold as * instead of **

I also updated some of the node serializers to use my custom implementation. Here’s some code, using tiptap.dev to access the underlying ProseMirror API directly:

import { defaultMarkdownSerializer, MarkdownSerializer, MarkdownSerializerState } from "../markdown/to_markdown";

            new Plugin({
                key: new PluginKey('clipboardTextSerializer'),
                props: {
                    clipboardTextSerializer: (slice: Slice<any>): string => {
                        const serializer: MarkdownSerializer = getMarkdownSerializer(this.editor.schema as any);
                        return serializer.serialize(slice.content as any, {
                            tightLists: true,
                            paragraphNewlines: 1, // tight newlines
                        });
                        // old code
                        // const { editor } = this;
                        // const { state, schema } = editor;
                        // const { doc, selection } = state;
                        // const { from, to } = selection;
                        // const textSerializers = getTextSeralizersFromSchema(schema as any);
                        // const range = { from, to };
                        // return getTextBetween(doc, range, { textSerializers });
                    },
                    // handleDOMEvents: {
                    //     copy: (view: EditorView<any>, event: ClipboardEvent) => {
                    //         console.log("COPY!!!");
                    //         return false;
                    //     }
                    // },
                },
            }),


function getMarkdownSerializer(schema: Schema) {
    const serializer: MarkdownSerializer = defaultMarkdownSerializer;
    serializer.marks['bold'] = { open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true };
    serializer.marks['highlight'] = { open: "==", close: "==", mixable: true, expelEnclosingWhitespace: true };
    serializer.marks['italic'] = { open: "_", close: "_", mixable: true, expelEnclosingWhitespace: true };
    serializer.marks['strike'] = { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true };

    Object.entries(schema.nodes).forEach(([name, tiptapNode]) => {
        if (!tiptapNode.spec.toText) {
            return;
        }

        serializer.nodes[name] = (state: MarkdownSerializerState, node: Node, parent?: Node, index?: number) => {
            state.write(tiptapNode.spec.toText({ node, pos: -1, parent, index }));
            if (!tiptapNode.spec.inline) {
                state.closeBlock(node);
            }
        };
    });

    //serializer.nodes['blockquote']
    serializer.nodes['bulletList'] = serializer.nodes.bullet_list;
    serializer.nodes['codeBlock'] = serializer.nodes.code_block;
    serializer.nodes['hardBreak'] = serializer.nodes.hard_break;
    //serializer.nodes['heading']
    serializer.nodes['horizontalRule'] = serializer.nodes.horizontal_rule;
    //serializer.nodes['image']
    serializer.nodes['listItem'] = serializer.nodes.list_item;
    serializer.nodes['orderedList'] = serializer.nodes.ordered_list;
    //serializer.nodes['paragraph']
    //serializer.nodes['scribeTask']
    //serializer.nodes['tableOfContents']
    //serializer.nodes['text']
    return serializer;
}

Here’s an example of the toText() function for e.g. the tableOfContents node:

        return "[[toc]]\r\n";

Hope this helps someone.

1 Like