diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js index 79fc0eea2c788541fe6dcc2efea382464e6db0f1..58fa2655e250ba8ecba29d1c51c4b4b467a2c88e 100644 --- a/app/assets/javascripts/content_editor/extensions/html_marks.js +++ b/app/assets/javascripts/content_editor/extensions/html_marks.js @@ -50,7 +50,8 @@ export default marks.map((name) => }, parseHTML() { - return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }]; + const tag = name === 'span' ? `${name}:not([data-escaped-char])` : name; + return [{ tag, priority: PARSE_HTML_PRIORITY_LOWEST }]; }, renderHTML({ HTMLAttributes }) { diff --git a/spec/frontend/content_editor/extensions/html_marks_spec.js b/spec/frontend/content_editor/extensions/html_marks_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..3757962ce5210a2b247fbb4211d573db807f3e5b --- /dev/null +++ b/spec/frontend/content_editor/extensions/html_marks_spec.js @@ -0,0 +1,89 @@ +import HTMLMarks from '~/content_editor/extensions/html_marks'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/extensions/html_marks', () => { + let tiptapEditor; + let doc; + let ins; + let abbr; + let bdo; + let cite; + let dfn; + let small; + let span; + let time; + let kbd; + let q; + let p; + let samp; + let varMark; + let ruby; + let rp; + let rt; + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [...HTMLMarks] }); + + ({ + builders: { + doc, + ins, + abbr, + bdo, + cite, + dfn, + small, + span, + time, + kbd, + q, + samp, + var: varMark, + ruby, + rp, + rt, + p, + }, + } = createDocBuilder({ + tiptapEditor, + names: { + ...HTMLMarks.reduce( + (builders, htmlMark) => ({ + ...builders, + [htmlMark.name]: { markType: htmlMark.name }, + }), + {}, + ), + }, + })); + }); + + it.each` + input | expectedContent + ${'<ins>inserted</ins>'} | ${() => ins('inserted')} + ${'<abbr title="abbr">abbreviation</abbr>'} | ${() => abbr({ title: 'abbr' }, 'abbreviation')} + ${'<bdo dir="rtl">bdo</bdo>'} | ${() => bdo({ dir: 'rtl' }, 'bdo')} + ${'<cite>citation</cite>'} | ${() => cite('citation')} + ${'<dfn>definition</dfn>'} | ${() => dfn('definition')} + ${'<small>small text</small>'} | ${() => small('small text')} + ${'<span dir="rtl">span text</span>'} | ${() => span({ dir: 'rtl' }, 'span text')} + ${'<time datetime="2023-11-02">November 2, 2023</time>'} | ${() => time({ datetime: '2023-11-02' }, 'November 2, 2023')} + ${'<kbd>keyboard</kbd>'} | ${() => kbd('keyboard')} + ${'<q>quote</q>'} | ${() => q('quote')} + ${'<samp>sample</samp>'} | ${() => samp('sample')} + ${'<var>variable</var>'} | ${() => varMark('variable')} + ${'<ruby>base<rp>(</rp><rt>ruby</rt><rp>)</rp></ruby>'} | ${() => ruby('base', rp('('), rt('ruby'), rp(')'))} + `('parses and creates marks for $input', ({ input, expectedContent }) => { + tiptapEditor.commands.setContent(input); + expect(tiptapEditor.getJSON()).toEqual(doc(p(expectedContent())).toJSON()); + expect(tiptapEditor.getHTML()).toContain(input); + }); + + it('does not parse an element with a data-escaped-char attribute', () => { + const input = '<span data-escaped-char>#</span> not a heading'; + const expectedDoc = doc(p('# not a heading')); + tiptapEditor.commands.setContent(input); + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + expect(tiptapEditor.getHTML()).not.toContain('<span'); + }); +});