diff options
Diffstat (limited to 'app/assets/javascripts/content_editor/services/markdown_serializer.js')
-rw-r--r-- | app/assets/javascripts/content_editor/services/markdown_serializer.js | 174 |
1 files changed, 171 insertions, 3 deletions
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index f121cc9affd..df4d31c3d7f 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -1,5 +1,165 @@ -import { MarkdownSerializer as ProseMirrorMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown'; +import { + MarkdownSerializer as ProseMirrorMarkdownSerializer, + defaultMarkdownSerializer, +} from 'prosemirror-markdown/src/to_markdown'; import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; +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 Emoji from '../extensions/emoji'; +import HardBreak from '../extensions/hard_break'; +import Heading from '../extensions/heading'; +import HorizontalRule from '../extensions/horizontal_rule'; +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 OrderedList from '../extensions/ordered_list'; +import Paragraph from '../extensions/paragraph'; +import Reference from '../extensions/reference'; +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 TableRow from '../extensions/table_row'; +import TaskItem from '../extensions/task_item'; +import TaskList from '../extensions/task_list'; +import Text from '../extensions/text'; + +const defaultSerializerConfig = { + marks: { + [Bold.name]: defaultMarkdownSerializer.marks.strong, + [Code.name]: defaultMarkdownSerializer.marks.code, + [Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true }, + [Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true }, + [Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true }, + [InlineDiff.name]: { + mixable: true, + open(state, mark) { + return mark.attrs.type === 'addition' ? '{+' : '{-'; + }, + close(state, mark) { + return mark.attrs.type === 'addition' ? '+}' : '-}'; + }, + }, + [Link.name]: { + open() { + return '['; + }, + close(state, mark) { + const href = mark.attrs.canonicalSrc || mark.attrs.href; + return `](${state.esc(href)}${ + mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : '' + })`; + }, + }, + [Strike.name]: { + open: '~~', + close: '~~', + mixable: true, + expelEnclosingWhitespace: true, + }, + }, + nodes: { + [Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote, + [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list, + [CodeBlockHighlight.name]: (state, node) => { + state.write(`\`\`\`${node.attrs.language || ''}\n`); + state.text(node.textContent, false); + state.ensureNewLine(); + state.write('```'); + state.closeBlock(node); + }, + [Emoji.name]: (state, node) => { + const { name } = node.attrs; + + state.write(`:${name}:`); + }, + [HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break, + [Heading.name]: defaultMarkdownSerializer.nodes.heading, + [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule, + [Image.name]: (state, node) => { + const { alt, canonicalSrc, src, title } = node.attrs; + const quotedTitle = title ? ` ${state.quote(title)}` : ''; + + state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`); + }, + [ListItem.name]: defaultMarkdownSerializer.nodes.list_item, + [OrderedList.name]: defaultMarkdownSerializer.nodes.ordered_list, + [Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph, + [Reference.name]: (state, node) => { + state.write(node.attrs.originalText || node.attrs.text); + }, + [Table.name]: (state, node) => { + state.renderContent(node); + }, + [TableCell.name]: (state, node) => { + state.renderInline(node); + }, + [TableHeader.name]: (state, node) => { + state.renderInline(node); + }, + [TableRow.name]: (state, node) => { + const isHeaderRow = node.child(0).type.name === 'tableHeader'; + + const renderRow = () => { + const cellWidths = []; + + state.flushClose(1); + + state.write('| '); + node.forEach((cell, _, i) => { + if (i) state.write(' | '); + + const { length } = state.out; + state.render(cell, node, i); + cellWidths.push(state.out.length - length); + }); + state.write(' |'); + + state.closeBlock(node); + + return cellWidths; + }; + + const renderHeaderRow = (cellWidths) => { + state.flushClose(1); + + state.write('|'); + node.forEach((cell, _, i) => { + if (i) state.write('|'); + + state.write(cell.attrs.align === 'center' ? ':' : '-'); + state.write(state.repeat('-', cellWidths[i])); + state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-'); + }); + state.write('|'); + + state.closeBlock(node); + }; + + if (isHeaderRow) { + renderHeaderRow(renderRow()); + } else { + renderRow(); + } + }, + [TaskItem.name]: (state, node) => { + state.write(`[${node.attrs.checked ? 'x' : ' '}] `); + state.renderContent(node); + }, + [TaskList.name]: (state, node) => { + if (node.attrs.type === 'ul') defaultMarkdownSerializer.nodes.bullet_list(state, node); + else defaultMarkdownSerializer.nodes.ordered_list(state, node); + }, + [Text.name]: defaultMarkdownSerializer.nodes.text, + }, +}; const wrapHtmlPayload = (payload) => `<div>${payload}</div>`; @@ -50,8 +210,16 @@ export default ({ render = () => null, serializerConfig }) => ({ */ serialize: ({ schema, content }) => { const proseMirrorDocument = schema.nodeFromJSON(content); - const { nodes, marks } = serializerConfig; - const serializer = new ProseMirrorMarkdownSerializer(nodes, marks); + const serializer = new ProseMirrorMarkdownSerializer( + { + ...defaultSerializerConfig.nodes, + ...serializerConfig.nodes, + }, + { + ...defaultSerializerConfig.marks, + ...serializerConfig.marks, + }, + ); return serializer.serialize(proseMirrorDocument, { tightLists: true, |