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)
- Serialize paragraphs as single line
- 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.