import { MarkdownSerializer as ProseMirrorMarkdownSerializer, defaultMarkdownSerializer, } from '~/lib/prosemirror_markdown_serializer'; import Audio from '../extensions/audio'; import Blockquote from '../extensions/blockquote'; import Bold from '../extensions/bold'; import BulletList from '../extensions/bullet_list'; import Code from '../extensions/code'; import CodeBlockHighlight from '../extensions/code_block_highlight'; import DescriptionItem from '../extensions/description_item'; import DescriptionList from '../extensions/description_list'; import Details from '../extensions/details'; import DetailsContent from '../extensions/details_content'; import Diagram from '../extensions/diagram'; import Emoji from '../extensions/emoji'; import Figure from '../extensions/figure'; import FigureCaption from '../extensions/figure_caption'; import FootnoteDefinition from '../extensions/footnote_definition'; import FootnoteReference from '../extensions/footnote_reference'; import Frontmatter from '../extensions/frontmatter'; import HardBreak from '../extensions/hard_break'; import Heading from '../extensions/heading'; import HorizontalRule from '../extensions/horizontal_rule'; import HTMLMarks from '../extensions/html_marks'; import HTMLNodes from '../extensions/html_nodes'; import Image from '../extensions/image'; import InlineDiff from '../extensions/inline_diff'; import Italic from '../extensions/italic'; import Link from '../extensions/link'; import ListItem from '../extensions/list_item'; import MathInline from '../extensions/math_inline'; import OrderedList from '../extensions/ordered_list'; import Paragraph from '../extensions/paragraph'; import Reference from '../extensions/reference'; import ReferenceDefinition from '../extensions/reference_definition'; import Strike from '../extensions/strike'; import Subscript from '../extensions/subscript'; import Superscript from '../extensions/superscript'; import Table from '../extensions/table'; import TableCell from '../extensions/table_cell'; import TableHeader from '../extensions/table_header'; import TableOfContents from '../extensions/table_of_contents'; import TableRow from '../extensions/table_row'; import TaskItem from '../extensions/task_item'; import TaskList from '../extensions/task_list'; import Text from '../extensions/text'; import Video from '../extensions/video'; import WordBreak from '../extensions/word_break'; import { renderCodeBlock, renderHardBreak, renderTable, renderTableCell, renderTableRow, openTag, closeTag, renderOrderedList, renderImage, renderPlayable, renderHTMLNode, renderContent, renderBulletList, preserveUnchanged, bold, italic, link, code, strike, } from './serialization_helpers'; const defaultSerializerConfig = { marks: { [Bold.name]: bold, [Italic.name]: italic, [Code.name]: code, [Subscript.name]: { open: '', close: '', mixable: true }, [Superscript.name]: { open: '', close: '', mixable: true }, [InlineDiff.name]: { mixable: true, open(_, mark) { return mark.attrs.type === 'addition' ? '{+' : '{-'; }, close(_, mark) { return mark.attrs.type === 'addition' ? '+}' : '-}'; }, }, [Link.name]: link, [MathInline.name]: { open: (...args) => `$${defaultMarkdownSerializer.marks.code.open(...args)}`, close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`, escape: false, }, [Strike.name]: strike, ...HTMLMarks.reduce( (acc, { name }) => ({ ...acc, [name]: { mixable: true, open(state, node) { return openTag(name, node.attrs); }, close: closeTag(name), }, }), {}, ), }, nodes: { [Audio.name]: preserveUnchanged({ render: renderPlayable, inline: true, }), [Blockquote.name]: preserveUnchanged((state, node) => { if (node.attrs.multiline) { state.write('>>>'); state.ensureNewLine(); state.renderContent(node); state.ensureNewLine(); state.write('>>>'); state.closeBlock(node); } else { state.wrapBlock('> ', null, node, () => state.renderContent(node)); } }), [BulletList.name]: preserveUnchanged(renderBulletList), [CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock), [Diagram.name]: preserveUnchanged(renderCodeBlock), [DescriptionList.name]: renderHTMLNode('dl', true), [DescriptionItem.name]: (state, node, parent, index) => { if (index === 1) state.ensureNewLine(); renderHTMLNode(node.attrs.isTerm ? 'dt' : 'dd')(state, node); if (index === parent.childCount - 1) state.ensureNewLine(); }, [Details.name]: renderHTMLNode('details', true), [DetailsContent.name]: (state, node, parent, index) => { if (!index) renderHTMLNode('summary')(state, node); else { if (index === 1) state.ensureNewLine(); renderContent(state, node); if (index === parent.childCount - 1) state.ensureNewLine(); } }, [Emoji.name]: (state, node) => { const { name } = node.attrs; state.write(`:${name}:`); }, [FootnoteDefinition.name]: preserveUnchanged((state, node) => { state.write(`[^${node.attrs.identifier}]: `); state.renderInline(node); state.ensureNewLine(); }), [FootnoteReference.name]: preserveUnchanged({ render: (state, node) => { state.write(`[^${node.attrs.identifier}]`); }, inline: true, }), [Frontmatter.name]: preserveUnchanged((state, node) => { const { language } = node.attrs; const syntax = { toml: '+++', json: ';;;', yaml: '---', }[language]; state.write(`${syntax}\n`); state.text(node.textContent, false); state.ensureNewLine(); state.write(syntax); state.closeBlock(node); }), [Figure.name]: renderHTMLNode('figure'), [FigureCaption.name]: renderHTMLNode('figcaption'), [HardBreak.name]: preserveUnchanged(renderHardBreak), [Heading.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.heading), [HorizontalRule.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.horizontal_rule), [Image.name]: preserveUnchanged({ render: renderImage, inline: true, }), [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item), [OrderedList.name]: preserveUnchanged(renderOrderedList), [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph), [Reference.name]: (state, node) => { state.write(node.attrs.originalText || node.attrs.text); }, [ReferenceDefinition.name]: preserveUnchanged({ render: (state, node, parent, index, same, sourceMarkdown) => { const nextSibling = parent.maybeChild(index + 1); state.text(same ? sourceMarkdown : node.textContent, false); /** * Do not insert a blank line between reference definitions * because it isn’t necessary and a more compact text format * is preferred. */ if (!nextSibling || nextSibling.type.name !== ReferenceDefinition.name) { state.closeBlock(node); } else { state.ensureNewLine(); } }, overwriteSourcePreservationStrategy: true, }), [TableOfContents.name]: preserveUnchanged((state, node) => { state.write('[[_TOC_]]'); state.closeBlock(node); }), [Table.name]: preserveUnchanged(renderTable), [TableCell.name]: renderTableCell, [TableHeader.name]: renderTableCell, [TableRow.name]: renderTableRow, [TaskItem.name]: preserveUnchanged((state, node) => { state.write(`[${node.attrs.checked ? 'x' : ' '}] `); state.renderContent(node); }), [TaskList.name]: preserveUnchanged((state, node) => { if (node.attrs.numeric) renderOrderedList(state, node); else renderBulletList(state, node); }), [Text.name]: defaultMarkdownSerializer.nodes.text, [Video.name]: preserveUnchanged({ render: renderPlayable, inline: true, }), [WordBreak.name]: (state) => state.write(''), ...HTMLNodes.reduce((serializers, htmlNode) => { return { ...serializers, [htmlNode.name]: (state, node) => renderHTMLNode(htmlNode.options.tagName)(state, node), }; }, {}), }, }; const createChangeTracker = (doc, pristineDoc) => { const changeTracker = new WeakMap(); const pristineSourceMarkdownMap = new Map(); if (doc && pristineDoc) { pristineDoc.descendants((node) => { if (node.attrs.sourceMapKey) { pristineSourceMarkdownMap.set(`${node.attrs.sourceMapKey}${node.type.name}`, node); } }); doc.descendants((node) => { const pristineNode = pristineSourceMarkdownMap.get( `${node.attrs.sourceMapKey}${node.type.name}`, ); if (pristineNode) { changeTracker.set(node, node.eq(pristineNode)); } }); } return changeTracker; }; /** * Converts a ProseMirror document to Markdown. See the * following documentation to learn how to implement * custom node and mark serializer functions. * * https://github.com/prosemirror/prosemirror-markdown * * @param {Object} params.nodes ProseMirror node serializer functions * @param {Object} params.marks ProseMirror marks serializer config * * @returns a markdown serializer */ export default ({ serializerConfig = {} } = {}) => ({ /** * Serializes a ProseMirror document as Markdown. If a node contains * sourcemap metadata, the serializer is capable of restoring the * Markdown from which the node was generated using a Markdown * deserializer. * * See the Sourcemap metadata extension and the remark_markdown_deserializer * service for more information. * * @param {ProseMirror.Node} params.doc ProseMirror document to convert into Markdown * @param {ProseMirror.Node} params.pristineDoc Pristine version of the document that * should be converted into Markdown. This is used to detect which nodes in the document * changed. * @returns A String that represents the serialized document as Markdown */ serialize: ({ doc, pristineDoc }) => { const changeTracker = createChangeTracker(doc, pristineDoc); const serializer = new ProseMirrorMarkdownSerializer( { ...defaultSerializerConfig.nodes, ...serializerConfig.nodes, }, { ...defaultSerializerConfig.marks, ...serializerConfig.marks, }, ); return serializer.serialize(doc, { tightLists: true, changeTracker, }); }, });