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 | 70 |
1 files changed, 60 insertions, 10 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 8a15633708f..ca290efca11 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -1,7 +1,10 @@ import { render } from '~/lib/gfm'; import { isValidAttribute } from '~/lib/dompurify'; +import { SAFE_AUDIO_EXT, SAFE_VIDEO_EXT, DIAGRAM_LANGUAGES } from '../constants'; import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter'; +const ALL_AUDIO_VIDEO_EXT = [...SAFE_AUDIO_EXT, ...SAFE_VIDEO_EXT]; + const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del']; const isTaskItem = (hastNode) => { @@ -17,6 +20,32 @@ const getTableCellAttrs = (hastNode) => ({ rowspan: parseInt(hastNode.properties.rowSpan, 10) || 1, }); +const getMediaAttrs = (hastNode) => ({ + src: hastNode.properties.src, + canonicalSrc: hastNode.properties.identifier ?? hastNode.properties.src, + isReference: hastNode.properties.isReference === 'true', + title: hastNode.properties.title, + alt: hastNode.properties.alt, +}); + +const isMediaTag = (hastNode) => hastNode.tagName === 'img' && Boolean(hastNode.properties); + +const extractMediaFileExtension = (url) => { + try { + const parsedUrl = new URL(url, window.location.origin); + + return /\.(\w+)$/.exec(parsedUrl.pathname)?.[1] ?? null; + } catch { + return null; + } +}; + +const isCodeBlock = (hastNode) => hastNode.tagName === 'codeblock'; + +const isDiagramCodeBlock = (hastNode) => DIAGRAM_LANGUAGES.includes(hastNode.properties?.language); + +const getCodeBlockAttrs = (hastNode) => ({ language: hastNode.properties.language }); + const factorySpecs = { blockquote: { type: 'block', selector: 'blockquote' }, paragraph: { type: 'block', selector: 'p' }, @@ -45,8 +74,13 @@ const factorySpecs = { }, codeBlock: { type: 'block', - selector: 'codeblock', - getAttrs: (hastNode) => ({ ...hastNode.properties }), + selector: (hastNode) => isCodeBlock(hastNode) && !isDiagramCodeBlock(hastNode), + getAttrs: getCodeBlockAttrs, + }, + diagram: { + type: 'block', + selector: (hastNode) => isCodeBlock(hastNode) && isDiagramCodeBlock(hastNode), + getAttrs: getCodeBlockAttrs, }, horizontalRule: { type: 'block', @@ -121,16 +155,26 @@ const factorySpecs = { selector: 'pre', wrapInParagraph: true, }, + audio: { + type: 'inline', + selector: (hastNode) => + isMediaTag(hastNode) && + SAFE_AUDIO_EXT.includes(extractMediaFileExtension(hastNode.properties.src)), + getAttrs: getMediaAttrs, + }, image: { type: 'inline', - selector: 'img', - getAttrs: (hastNode) => ({ - src: hastNode.properties.src, - canonicalSrc: hastNode.properties.identifier ?? hastNode.properties.src, - isReference: hastNode.properties.isReference === 'true', - title: hastNode.properties.title, - alt: hastNode.properties.alt, - }), + selector: (hastNode) => + isMediaTag(hastNode) && + !ALL_AUDIO_VIDEO_EXT.includes(extractMediaFileExtension(hastNode.properties.src)), + getAttrs: getMediaAttrs, + }, + video: { + type: 'inline', + selector: (hastNode) => + isMediaTag(hastNode) && + SAFE_VIDEO_EXT.includes(extractMediaFileExtension(hastNode.properties.src)), + getAttrs: getMediaAttrs, }, hardBreak: { type: 'inline', @@ -193,6 +237,11 @@ const factorySpecs = { language: hastNode.properties.language, }), }, + + tableOfContents: { + type: 'block', + selector: 'tableofcontents', + }, }; const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url', 'isReference']; @@ -250,6 +299,7 @@ export default () => { 'yaml', 'toml', 'json', + 'tableOfContents', ], }); |