diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue index a70673add2..f8b779ca96 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue @@ -57,23 +57,19 @@ import '../mathquill/mathquill.js'; import 'codemirror/lib/codemirror.css'; import '@toast-ui/editor/dist/toastui-editor.css'; - import * as Showdown from 'showdown'; import Editor from '@toast-ui/editor'; - import { stripHtml } from 'string-strip-html'; import imageUpload, { paramsToImageFieldHTML } from '../plugins/image-upload'; import formulas from '../plugins/formulas'; import minimize from '../plugins/minimize'; - import formulaHtmlToMd from '../plugins/formulas/formula-html-to-md'; import formulaMdToHtml from '../plugins/formulas/formula-md-to-html'; - import imagesHtmlToMd from '../plugins/image-upload/image-html-to-md'; import imagesMdToHtml from '../plugins/image-upload/image-md-to-html'; import { CLASS_MATH_FIELD_ACTIVE } from '../constants'; import { registerMarkdownFormulaField } from '../plugins/formulas/MarkdownFormulaField'; import { registerMarkdownImageField } from '../plugins/image-upload/MarkdownImageField'; - import { clearNodeFormat, getExtensionMenuPosition } from './utils'; + import { clearNodeFormat, generateCustomConverter, getExtensionMenuPosition } from './utils'; import FormulasMenu from './FormulasMenu/FormulasMenu'; import ImagesMenu from './ImagesMenu/ImagesMenu'; import ClickOutside from 'shared/directives/click-outside'; @@ -167,38 +163,7 @@ mounted() { this.mathQuill = MathQuill.getInterface(2); - // This is currently the only way of inheriting and adjusting - // default TUI's convertor methods - // see https://github.com/nhn/tui.editor/issues/615 - const tmpEditor = new Editor({ - el: this.$refs.editor, - }); - const showdown = new Showdown.Converter(); - const Convertor = tmpEditor.convertor.constructor; - class CustomConvertor extends Convertor { - toMarkdown(content) { - content = imagesHtmlToMd(content); - content = formulaHtmlToMd(content); - content = showdown.makeMarkdown(content); - // TUI.editor sprinkles in extra `
` tags that Kolibri renders literally - // When showdown has already added linebreaks to render these in markdown - // so we just remove these here. - content = content.replaceAll('
', ''); - - // any copy pasted rich text that renders as HTML but does not get converted - // will linger here, so remove it as Kolibri will render it literally also. - content = stripHtml(content).result; - return content; - } - toHTML(content) { - // Kolibri and showdown assume double newlines for a single line break, - // wheras TUI.editor prefers single newline characters. - content = content.replaceAll('\n\n', '\n'); - content = super.toHTML(content); - return content; - } - } - tmpEditor.remove(); + const CustomConvertor = generateCustomConverter(this.$refs.editor); const createBoldButton = () => { { @@ -268,8 +233,8 @@ // https://github.com/nhn/tui.editor/blob/master/apps/editor/docs/custom-html-renderer.md customHTMLRenderer: { text(node) { - let content = formulaMdToHtml(node.literal); - content = imagesMdToHtml(content); + let content = formulaMdToHtml(node.literal, true); + content = imagesMdToHtml(content, true); return { type: 'html', content, diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/utils.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/utils.js index 5894845273..f5ce62e98e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/utils.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/utils.js @@ -1,3 +1,10 @@ +import * as Showdown from 'showdown'; +import Editor from '@toast-ui/editor'; +import { stripHtml } from 'string-strip-html'; + +import imagesHtmlToMd from '../plugins/image-upload/image-html-to-md'; +import formulaHtmlToMd from '../plugins/formulas/formula-html-to-md'; + /** * Clear DOM node by keeping only its text content. * @@ -74,3 +81,37 @@ export const getExtensionMenuPosition = ({ editorEl, targetX, targetY }) => { right: menuRight, }; }; + +export const generateCustomConverter = el => { + // This is currently the only way of inheriting and adjusting + // default TUI's convertor methods + // see https://github.com/nhn/tui.editor/issues/615 + const tmpEditor = new Editor({ el }); + const showdown = new Showdown.Converter(); + const Convertor = tmpEditor.convertor.constructor; + class CustomConvertor extends Convertor { + toMarkdown(content) { + content = showdown.makeMarkdown(content); + content = imagesHtmlToMd(content); + content = formulaHtmlToMd(content); + // TUI.editor sprinkles in extra `
` tags that Kolibri renders literally + // When showdown has already added linebreaks to render these in markdown + // so we just remove these here. + content = content.replaceAll('
', ''); + + // any copy pasted rich text that renders as HTML but does not get converted + // will linger here, so remove it as Kolibri will render it literally also. + content = stripHtml(content).result; + return content; + } + toHTML(content) { + // Kolibri and showdown assume double newlines for a single line break, + // wheras TUI.editor prefers single newline characters. + content = content.replaceAll('\n\n', '\n'); + content = super.toHTML(content); + return content; + } + } + tmpEditor.remove(); + return CustomConvertor; +}; diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/__tests__/utils.spec.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/__tests__/utils.spec.js index d201e81b33..1a55e0b179 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/__tests__/utils.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/__tests__/utils.spec.js @@ -1,4 +1,9 @@ -import { clearNodeFormat } from '../MarkdownEditor/utils'; +/** + * @jest-environment jest-environment-jsdom-sixteen + */ +// Jsdom@^16 is required to test toast UI, as it relies on the Range API. + +import { clearNodeFormat, generateCustomConverter } from '../MarkdownEditor/utils'; const htmlStringToFragment = htmlString => { const template = document.createElement('template'); @@ -56,3 +61,17 @@ describe('clearNodeFormat', () => { ); }); }); + +describe('markdown conversion', () => { + it('converts image tags to markdown without escaping them', () => { + const el = document.createElement('div'); + const CustomConvertor = generateCustomConverter(el); + const converter = new CustomConvertor(); + const html = + '![](${☣ CONTENTSTORAGE}/bc1c5a86e1e46f20a6b4ee2c1bb6d6ff.png =485.453125x394)'; + + expect(converter.toMarkdown(html)).toBe( + '![](${☣ CONTENTSTORAGE}/bc1c5a86e1e46f20a6b4ee2c1bb6d6ff.png =485.453125x394)' + ); + }); +}); diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-md-to-html.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-md-to-html.js index f99fcd2f09..61669e5eac 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-md-to-html.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-md-to-html.js @@ -12,6 +12,10 @@ * */ -export default markdown => { - return markdown.replace(/\$\$(.*?)\$\$/g, '$1'); +export default (markdown, editing) => { + const editAttr = editing ? ' editing="true"' : ''; + return markdown.replace( + /\$\$(.*?)\$\$/g, + `$1` + ); }; diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/image-md-to-html.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/image-md-to-html.js index c452c5a82a..1f61c121d1 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/image-md-to-html.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/image-md-to-html.js @@ -14,6 +14,6 @@ import { IMAGE_REGEX, imageMdToImageFieldHTML } from './index'; // convert markdown images to image editor field custom elements -export default markdown => { - return markdown.replace(IMAGE_REGEX, imageMd => imageMdToImageFieldHTML(imageMd)); +export default (markdown, editing) => { + return markdown.replace(IMAGE_REGEX, imageMd => imageMdToImageFieldHTML(imageMd, editing)); }; diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/index.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/index.js index b4b13a7201..0e33f894c4 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/index.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/index.js @@ -30,8 +30,10 @@ export const paramsToImageMd = ({ src, alt, width, height }) => { } }; -export const imageMdToImageFieldHTML = imageMd => - `${imageMd}`; +export const imageMdToImageFieldHTML = (imageMd, editing) => { + const editAttr = editing ? ' editing="true"' : ''; + return `${imageMd}`; +}; export const paramsToImageFieldHTML = params => imageMdToImageFieldHTML(paramsToImageMd(params)); export default imageUploadExtension; diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js index c9a3f51940..da581955be 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js @@ -102,11 +102,11 @@ export default VueComponent => { '' ); } - this.parentNode.removeChild(this); + if (this.parentNode) { + this.parentNode.removeChild(this); + } }); - this.editing = true; - if (!hasLeftwardSpace(this)) { this.insertAdjacentText('beforebegin', '\xa0'); }