summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
diff options
context:
space:
mode:
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.js70
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',
],
});