From 60080d6460c394b7d63ffcf5e540bc45f35346bc Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 22 Nov 2022 14:27:21 -0800 Subject: [PATCH 1/7] Remove image from innerHTML on remove. --- .../MarkdownEditor/plugins/image-upload/MarkdownImageField.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue index d5308e83dc..56bbcf70d0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue @@ -136,6 +136,7 @@ ); }, handleRemove() { + this.editorField.innerHTML = ''; this.$destroy(); this.$el.parentNode.removeChild(this.$el); }, From 01b4503bcda26bdf7a5265de49f20123d08dea5f Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 22 Nov 2022 17:14:46 -0800 Subject: [PATCH 2/7] Create remove event mechanism for clearing custom span element. --- .../plugins/image-upload/MarkdownImageField.vue | 9 ++++++--- .../plugins/registerCustomMarkdownField.js | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue index 56bbcf70d0..a58aad5705 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue @@ -136,9 +136,12 @@ ); }, handleRemove() { - this.editorField.innerHTML = ''; - this.$destroy(); - this.$el.parentNode.removeChild(this.$el); + this.editorField.dispatchEvent( + new CustomEvent('remove', { + bubbles: true, + cancelable: true, + }) + ); }, handleResize() { this.resizing = true; diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js index 250ae5b30f..9cfccb5ec6 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js @@ -15,10 +15,10 @@ export default VueComponent => { vueInstanceCreatedCallback() { // by default, `contenteditable` will be false this.setAttribute('contenteditable', Boolean(VueComponent.contentEditable)); - + const id = `markdown-field-${uuidv4()}`; // a hack to prevent squire from merging custom element spans // see here: https://github.com/nhn/tui.editor/blob/master/libs/squire/source/Node.js#L92-L101 - this.classList.add(`markdown-field-${uuidv4()}`); + this.classList.add(id); // pass innerHTML of host element as the `markdown` property this.observer = new MutationObserver(mutations => { @@ -41,6 +41,10 @@ export default VueComponent => { }); this.observer.observe(this, { characterData: true, childList: true }); + this.addEventListener('remove', () => { + this.parentNode.removeChild(this); + }); + // initialize the `markdown` property this.getVueInstance().$root.markdown = this.innerHTML; }, From 02216e960f50230704f1ff0d437fe8564662b590 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Wed, 23 Nov 2022 13:03:25 -0800 Subject: [PATCH 3/7] Consolidate all blank space wrapping in custom markdown field logic. --- .../MarkdownEditor/MarkdownEditor.vue | 38 +---------- .../plugins/registerCustomMarkdownField.js | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue index 722c44f4f0..99def78b77 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue @@ -60,7 +60,6 @@ import * as Showdown from 'showdown'; import Editor from '@toast-ui/editor'; - import debounce from 'lodash/debounce'; import { stripHtml } from 'string-strip-html'; import imageUpload, { paramsToImageFieldHTML } from '../plugins/image-upload'; @@ -83,7 +82,6 @@ registerMarkdownFormulaField(); registerMarkdownImageField(); - const wrapWithSpaces = html => ` ${html} `; const AnalyticsActionMap = { Bold: 'Bold', @@ -164,7 +162,6 @@ markdown(newMd, previousMd) { if (newMd !== previousMd && newMd !== this.editor.getMarkdown()) { this.editor.setMarkdown(newMd); - this.updateCustomNodeSpacers(); this.initImageFields(); } }, @@ -306,35 +303,6 @@ this.keyDownEventListener = this.$el.addEventListener('keydown', this.onKeyDown, true); this.clickEventListener = this.$el.addEventListener('click', this.onClick); this.editImageEventListener = this.$el.addEventListener('editImage', this.handleEditImage); - - // Make sure all custom nodes have spacers around them. - // Note: this is debounced because it's called every keystroke - const editorEl = this.$refs.editor; - this.updateCustomNodeSpacers = debounce(() => { - editorEl.querySelectorAll('span[is]').forEach(el => { - el.editing = true; - const hasLeftwardSpace = el => { - return ( - el.previousSibling && - el.previousSibling.textContent && - /\s$/.test(el.previousSibling.textContent) - ); - }; - const hasRightwardSpace = el => { - return ( - el.nextSibling && el.nextSibling.textContent && /^\s/.test(el.nextSibling.textContent) - ); - }; - if (!hasLeftwardSpace(el)) { - el.insertAdjacentText('beforebegin', '\xa0'); - } - if (!hasRightwardSpace(el)) { - el.insertAdjacentText('afterend', '\xa0'); - } - }); - }, 150); - - this.updateCustomNodeSpacers(); }, activated() { this.editor.focus(); @@ -413,8 +381,6 @@ event.preventDefault(); event.stopPropagation(); } - - this.updateCustomNodeSpacers(); }, onPaste(event) { const fragment = clearNodeFormat({ @@ -791,7 +757,6 @@ } else { let squire = this.editor.getSquire(); squire.insertHTML(formulaHTML); - this.updateCustomNodeSpacers(); } }, resetFormulasMenu() { @@ -876,8 +841,7 @@ const mdImageEl = template.content.firstElementChild; mdImageEl.setAttribute('editing', true); - // insert non-breaking spaces to allow users to write text before and after - this.editor.getSquire().insertHTML(wrapWithSpaces(mdImageEl.outerHTML)); + this.editor.getSquire().insertHTML(mdImageEl.outerHTML); this.initImageFields(); } diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js index 9cfccb5ec6..1870e9cf35 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js @@ -2,6 +2,54 @@ import Vue from 'vue'; import vueCustomElement from 'vue-custom-element'; import { v4 as uuidv4 } from 'uuid'; +const leftwardSpaceRegex = /\s$/; + +const leftwardDoubleSpaceRegex = /\s\s$/; + +const hasLeftwardSpace = el => { + return ( + // Has a previous sibling + el.previousSibling && + // Which has text content + el.previousSibling.textContent && + // The text content has white space right before this element + leftwardSpaceRegex.test(el.previousSibling.textContent) && + ( + // And either this sibling doesn't have a previous sibling + !el.previousSibling.previousSibling || + // Or the previous sibling is not another custom field + !el.previousSibling.previousSibling.hasAttribute('is') || + // Or the previous sibling has two white spaces, one for each + // of the custom fields on either side. + leftwardDoubleSpaceRegex.test(el.previousSibling.textContent) + ) + ); +}; + +const rightwardSpaceRegex = /^\s/; + +const rightwardDoubleSpaceRegex = /^\s\s/; + +const hasRightwardSpace = el => { + return ( + // Has a next sibling + el.nextSibling && + // Which has text content + el.nextSibling.textContent && + // The text content has white space right after this element + rightwardSpaceRegex.test(el.nextSibling.textContent) && + ( + // And either this sibling doesn't have a next sibling + !el.nextSibling.nextSibling || + // Or the next sibling is not another custom field + !el.nextSibling.nextSibling.hasAttribute('is') || + // Or the next sibling has two white spaces, one for each + // of the custom fields on either side. + rightwardDoubleSpaceRegex.test(el.nextSibling.textContent) + ) + ); +}; + export default VueComponent => { const dashed = camel => camel.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase(); const name = dashed(VueComponent.name); @@ -42,9 +90,24 @@ export default VueComponent => { this.observer.observe(this, { characterData: true, childList: true }); this.addEventListener('remove', () => { + if (hasLeftwardSpace(this)) { + this.previousSibling.textContent = this.previousSibling.textContent.replace(leftwardSpaceRegex, ''); + } + if (hasRightwardSpace(this)) { + this.nextSibling.textContent = this.nextSibling.textContent.replace(rightwardSpaceRegex, ''); + } this.parentNode.removeChild(this); }); + this.editing = true; + + if (!hasLeftwardSpace(this)) { + this.insertAdjacentText('beforebegin', '\xa0'); + } + if (!hasRightwardSpace(this)) { + this.insertAdjacentText('afterend', '\xa0'); + } + // initialize the `markdown` property this.getVueInstance().$root.markdown = this.innerHTML; }, From d5b981ef873a9ab0d8aae2e89920227ad8557f02 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Wed, 23 Nov 2022 13:04:15 -0800 Subject: [PATCH 4/7] Clean up squire keydown handling. --- .../MarkdownEditor/MarkdownEditor.vue | 40 +++++----- .../MarkdownEditor/keyHandlers.js | 74 ------------------- 2 files changed, 18 insertions(+), 96 deletions(-) delete mode 100644 contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/keyHandlers.js diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue index 99def78b77..f99c016c5d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue @@ -74,7 +74,6 @@ import { registerMarkdownFormulaField } from '../plugins/formulas/MarkdownFormulaField'; import { registerMarkdownImageField } from '../plugins/image-upload/MarkdownImageField'; import { clearNodeFormat, getExtensionMenuPosition } from './utils'; - import keyHandlers from './keyHandlers'; import FormulasMenu from './FormulasMenu/FormulasMenu'; import ImagesMenu from './ImagesMenu/ImagesMenu'; import ClickOutside from 'shared/directives/click-outside'; @@ -82,7 +81,6 @@ registerMarkdownFormulaField(); registerMarkdownImageField(); - const AnalyticsActionMap = { Bold: 'Bold', Italic: 'Italicize', @@ -326,15 +324,9 @@ * a recommended solution here https://github.com/neilj/Squire/issues/107 */ onKeyDown(event) { - const squire = this.editor.getSquire(); - // Apply squire selection workarounds this.fixSquireSelectionOnKeyDown(event); - if (event.key in keyHandlers) { - keyHandlers[event.key](squire); - } - // ESC should close menus if any are open // or close the editor if none are open if (event.key === 'Escape') { @@ -473,7 +465,7 @@ const getRightwardElement = selection => getElementAtRelativeOffset(selection, 1); const getCharacterAtRelativeOffset = (selection, relativeOffset) => { - let { element, offset } = squire.getSelectionInfoByOffset( + const { element, offset } = squire.getSelectionInfoByOffset( selection.startContainer, selection.startOffset + relativeOffset ); @@ -495,27 +487,31 @@ /\s$/.test(getCharacterAtRelativeOffset(selection, 0)); const moveCursor = (selection, amount) => { - let { element, offset } = squire.getSelectionInfoByOffset( - selection.startContainer, - selection.startOffset + amount - ); - if (amount > 0) { - selection.setStart(element, offset); - } else { - selection.setEnd(element, offset); - } + const element = getElementAtRelativeOffset(selection, amount); + selection.setStart(element, 0); + selection.setEnd(element, 0); return selection; }; - // make sure Squire doesn't delete rightward custom nodes when 'backspace' is pressed - if (event.key !== 'ArrowRight' && event.key !== 'Delete') { - if (isCustomNode(getRightwardElement(selection))) { + const rightwardElement = getRightwardElement(selection); + const leftwardElement = getLeftwardElement(selection); + + if (event.key === 'ArrowRight') { + if (isCustomNode(rightwardElement)) { + squire.setSelection(moveCursor(selection, 1)); + } else if (spacerAndCustomElementAreRightward(selection)) { + squire.setSelection(moveCursor(selection, 2)); + } + } + if (event.key === 'ArrowLeft') { + if (isCustomNode(leftwardElement)) { squire.setSelection(moveCursor(selection, -1)); + } else if (spacerAndCustomElementAreLeftward(selection)) { + squire.setSelection(moveCursor(selection, -2)); } } // make sure Squire doesn't get stuck with a broken cursor position when deleting // elements with `contenteditable="false"` in FireFox - let leftwardElement = getLeftwardElement(selection); if (event.key === 'Backspace') { if (selection.startContainer.tagName === 'DIV') { // This happens normally when deleting from the beginning of an empty line... diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/keyHandlers.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/keyHandlers.js deleted file mode 100644 index 7cb2b93597..0000000000 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/keyHandlers.js +++ /dev/null @@ -1,74 +0,0 @@ -import { CLASS_MATH_FIELD, KEY_ARROW_RIGHT, KEY_ARROW_LEFT, KEY_BACKSPACE } from '../constants'; - -/** - * When arrow right pressed and next element is a math field - * then move cursor to a first position after the math field - * - * @param {Object} squire Squire instance - */ -const onArrowRight = squire => { - const selection = squire.getSelection(); - - if ( - selection && - selection.startContainer.nextSibling && - selection.startOffset === selection.startContainer.length && - selection.startContainer.nextSibling.classList.contains(CLASS_MATH_FIELD) - ) { - const rangeAfterMathField = new Range(); - rangeAfterMathField.setStartAfter(selection.startContainer.nextSibling); - - squire.setSelection(rangeAfterMathField); - } -}; - -/** - * When arrow left pressed and previous element is a math field - * then move cursor to a last position before the math field - * - * @param {Object} squire Squire instance - */ -const onArrowLeft = squire => { - const selection = squire.getSelection(); - - if ( - selection && - selection.startContainer.previousSibling && - selection.startOffset === 1 && - selection.startContainer.previousSibling.classList.contains(CLASS_MATH_FIELD) - ) { - const rangeBeforeMathField = new Range(); - rangeBeforeMathField.setStartBefore(selection.startContainer.previousSibling); - - squire.setSelection(rangeBeforeMathField); - } -}; - -/** - * When backspace pressed and previous element is a math field - * then remove the math field - * - * @param {Object} squire Squire instance - */ -const onBackspace = squire => { - const selection = squire.getSelection(); - - if ( - selection && - selection.startContainer.previousSibling && - selection.startOffset === 1 && - selection.startContainer.previousSibling.classList.contains(CLASS_MATH_FIELD) - ) { - const mathFieldRange = new Range(); - mathFieldRange.setStartBefore(selection.startContainer.previousSibling); - mathFieldRange.setEndBefore(selection.startContainer); - - mathFieldRange.deleteContents(); - } -}; - -export default { - [KEY_ARROW_RIGHT]: onArrowRight, - [KEY_ARROW_LEFT]: onArrowLeft, - [KEY_BACKSPACE]: onBackspace, -}; From ff87c48db2b6df94591fae2de5e52fdfadd3e4a8 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Wed, 23 Nov 2022 13:40:02 -0800 Subject: [PATCH 5/7] Modify prop values using web component interface rather than trying to do prohibited direct prop modification on component. Lint. --- .../plugins/registerCustomMarkdownField.js | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js index 1870e9cf35..f544eb5fa6 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js @@ -14,15 +14,13 @@ const hasLeftwardSpace = el => { el.previousSibling.textContent && // The text content has white space right before this element leftwardSpaceRegex.test(el.previousSibling.textContent) && - ( - // And either this sibling doesn't have a previous sibling - !el.previousSibling.previousSibling || + // And either this sibling doesn't have a previous sibling + (!el.previousSibling.previousSibling || // Or the previous sibling is not another custom field !el.previousSibling.previousSibling.hasAttribute('is') || // Or the previous sibling has two white spaces, one for each // of the custom fields on either side. - leftwardDoubleSpaceRegex.test(el.previousSibling.textContent) - ) + leftwardDoubleSpaceRegex.test(el.previousSibling.textContent)) ); }; @@ -38,15 +36,13 @@ const hasRightwardSpace = el => { el.nextSibling.textContent && // The text content has white space right after this element rightwardSpaceRegex.test(el.nextSibling.textContent) && - ( - // And either this sibling doesn't have a next sibling - !el.nextSibling.nextSibling || + // And either this sibling doesn't have a next sibling + (!el.nextSibling.nextSibling || // Or the next sibling is not another custom field !el.nextSibling.nextSibling.hasAttribute('is') || // Or the next sibling has two white spaces, one for each // of the custom fields on either side. - rightwardDoubleSpaceRegex.test(el.nextSibling.textContent) - ) + rightwardDoubleSpaceRegex.test(el.nextSibling.textContent)) ); }; @@ -83,7 +79,7 @@ export default VueComponent => { this.innerHTML = textNodesRemoved.map(n => n.nodeValue).join(); } else { // otherwise, pass the innerHTML to inner Vue component as `markdown` prop - this.getVueInstance().markdown = this.innerHTML; + this.markdown = this.innerHTML; } }); }); @@ -91,10 +87,16 @@ export default VueComponent => { this.addEventListener('remove', () => { if (hasLeftwardSpace(this)) { - this.previousSibling.textContent = this.previousSibling.textContent.replace(leftwardSpaceRegex, ''); + this.previousSibling.textContent = this.previousSibling.textContent.replace( + leftwardSpaceRegex, + '' + ); } if (hasRightwardSpace(this)) { - this.nextSibling.textContent = this.nextSibling.textContent.replace(rightwardSpaceRegex, ''); + this.nextSibling.textContent = this.nextSibling.textContent.replace( + rightwardSpaceRegex, + '' + ); } this.parentNode.removeChild(this); }); @@ -107,9 +109,8 @@ export default VueComponent => { if (!hasRightwardSpace(this)) { this.insertAdjacentText('afterend', '\xa0'); } - // initialize the `markdown` property - this.getVueInstance().$root.markdown = this.innerHTML; + this.markdown = this.innerHTML; }, shadowCss: VueComponent.shadowCSS, shadow: true, From f1f0a5c5fafcdbcf5ab95e4344d2a9f258c96e95 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Wed, 23 Nov 2022 14:30:07 -0800 Subject: [PATCH 6/7] Add extra edge case handling for sibling checking. --- .../MarkdownEditor/plugins/registerCustomMarkdownField.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js index f544eb5fa6..c9a3f51940 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js @@ -16,6 +16,8 @@ const hasLeftwardSpace = el => { leftwardSpaceRegex.test(el.previousSibling.textContent) && // And either this sibling doesn't have a previous sibling (!el.previousSibling.previousSibling || + // Or it doesn't have a hasAttribute function + typeof el.previousSibling.previousSibling.hasAttribute !== 'function' || // Or the previous sibling is not another custom field !el.previousSibling.previousSibling.hasAttribute('is') || // Or the previous sibling has two white spaces, one for each @@ -38,6 +40,8 @@ const hasRightwardSpace = el => { rightwardSpaceRegex.test(el.nextSibling.textContent) && // And either this sibling doesn't have a next sibling (!el.nextSibling.nextSibling || + // Or it doesn't have a hasAttribute function + typeof el.nextSibling.nextSibling.hasAttribute !== 'function' || // Or the next sibling is not another custom field !el.nextSibling.nextSibling.hasAttribute('is') || // Or the next sibling has two white spaces, one for each From 48a97be1c70fce67ef83fb34999ab87f3e2736d8 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Wed, 23 Nov 2022 14:30:35 -0800 Subject: [PATCH 7/7] Tweak markdown conversion to prevent addition of excess line breaks. --- .../MarkdownEditor/MarkdownEditor/MarkdownEditor.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue index f99c016c5d..a70673add2 100644 --- a/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue @@ -177,12 +177,14 @@ const Convertor = tmpEditor.convertor.constructor; class CustomConvertor extends Convertor { toMarkdown(content) { - content = showdown.makeMarkdown(content); content = imagesHtmlToMd(content); content = formulaHtmlToMd(content); - content = content.replaceAll(' ', ' '); - + 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;