diff options
Diffstat (limited to 'app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js')
-rw-r--r-- | app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js | 180 |
1 files changed, 138 insertions, 42 deletions
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js index 770de1df0d0..da10c684b0b 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -2,39 +2,51 @@ import { isString } from 'lodash'; import { render } from '~/lib/gfm'; import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter'; +const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del']; + +const isTaskItem = (hastNode) => { + const { className } = hastNode.properties; + + return ( + hastNode.tagName === 'li' && Array.isArray(className) && className.includes('task-list-item') + ); +}; + +const getTableCellAttrs = (hastNode) => ({ + colspan: parseInt(hastNode.properties.colSpan, 10) || 1, + rowspan: parseInt(hastNode.properties.rowSpan, 10) || 1, +}); + const factorySpecs = { - blockquote: { block: 'blockquote' }, - p: { block: 'paragraph' }, - li: { block: 'listItem', wrapTextInParagraph: true }, - ul: { block: 'bulletList' }, - ol: { block: 'orderedList' }, - h1: { - block: 'heading', - getAttrs: () => ({ level: 1 }), - }, - h2: { - block: 'heading', - getAttrs: () => ({ level: 2 }), - }, - h3: { - block: 'heading', - getAttrs: () => ({ level: 3 }), - }, - h4: { - block: 'heading', - getAttrs: () => ({ level: 4 }), - }, - h5: { - block: 'heading', - getAttrs: () => ({ level: 5 }), - }, - h6: { - block: 'heading', - getAttrs: () => ({ level: 6 }), - }, - pre: { - block: 'codeBlock', + blockquote: { type: 'block', selector: 'blockquote' }, + paragraph: { type: 'block', selector: 'p' }, + listItem: { + type: 'block', + wrapInParagraph: true, + selector: (hastNode) => hastNode.tagName === 'li' && !hastNode.properties.className, + processText: (text) => text.trimRight(), + }, + orderedList: { + type: 'block', + selector: (hastNode) => hastNode.tagName === 'ol' && !hastNode.properties.className, + }, + bulletList: { + type: 'block', + selector: (hastNode) => hastNode.tagName === 'ul' && !hastNode.properties.className, + }, + heading: { + type: 'block', + selector: (hastNode) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(hastNode.tagName), + getAttrs: (hastNode) => { + const level = parseInt(/(\d)$/.exec(hastNode.tagName)?.[1], 10) || 1; + + return { level }; + }, + }, + codeBlock: { + type: 'block', skipChildren: true, + selector: 'pre', getContent: ({ hastNodeText }) => hastNodeText.replace(/\n$/, ''), getAttrs: (hastNode) => { const languageClass = hastNode.children[0]?.properties.className?.[0]; @@ -43,28 +55,111 @@ const factorySpecs = { return { language }; }, }, - hr: { inline: 'horizontalRule' }, - img: { - inline: 'image', + horizontalRule: { + type: 'block', + selector: 'hr', + }, + taskList: { + type: 'block', + selector: (hastNode) => { + const { className } = hastNode.properties; + + return ( + ['ul', 'ol'].includes(hastNode.tagName) && + Array.isArray(className) && + className.includes('contains-task-list') + ); + }, + getAttrs: (hastNode) => ({ + numeric: hastNode.tagName === 'ol', + }), + }, + taskItem: { + type: 'block', + wrapInParagraph: true, + selector: isTaskItem, + getAttrs: (hastNode) => ({ + checked: hastNode.children[0].properties.checked, + }), + processText: (text) => text.trimLeft(), + }, + taskItemCheckbox: { + type: 'ignore', + selector: (hastNode, ancestors) => + hastNode.tagName === 'input' && isTaskItem(ancestors[ancestors.length - 1]), + }, + table: { + type: 'block', + selector: 'table', + }, + tableRow: { + type: 'block', + selector: 'tr', + parent: 'table', + }, + tableHeader: { + type: 'block', + selector: 'th', + getAttrs: getTableCellAttrs, + wrapInParagraph: true, + }, + tableCell: { + type: 'block', + selector: 'td', + getAttrs: getTableCellAttrs, + wrapInParagraph: true, + }, + ignoredTableNodes: { + type: 'ignore', + selector: (hastNode) => ['thead', 'tbody', 'tfoot'].includes(hastNode.tagName), + }, + footnoteDefinition: { + type: 'block', + selector: 'footnotedefinition', + getAttrs: (hastNode) => hastNode.properties, + }, + image: { + type: 'inline', + selector: 'img', getAttrs: (hastNode) => ({ src: hastNode.properties.src, title: hastNode.properties.title, alt: hastNode.properties.alt, }), }, - br: { inline: 'hardBreak' }, - code: { mark: 'code' }, - em: { mark: 'italic' }, - i: { mark: 'italic' }, - strong: { mark: 'bold' }, - b: { mark: 'bold' }, - a: { - mark: 'link', + hardBreak: { + type: 'inline', + selector: 'br', + }, + footnoteReference: { + type: 'inline', + selector: 'footnotereference', + getAttrs: (hastNode) => hastNode.properties, + }, + code: { + type: 'mark', + selector: 'code', + }, + italic: { + type: 'mark', + selector: (hastNode) => ['em', 'i'].includes(hastNode.tagName), + }, + bold: { + type: 'mark', + selector: (hastNode) => ['strong', 'b'].includes(hastNode.tagName), + }, + link: { + type: 'mark', + selector: 'a', getAttrs: (hastNode) => ({ href: hastNode.properties.href, title: hastNode.properties.title, }), }, + strike: { + type: 'mark', + selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName), + }, }; export default () => { @@ -77,6 +172,7 @@ export default () => { schema, factorySpecs, tree, + wrappableTags, source: markdown, }), }); |