diff --git a/src/nodes/Image.ts b/src/nodes/Image.ts index 405dc92a694..825eb1dd0de 100644 --- a/src/nodes/Image.ts +++ b/src/nodes/Image.ts @@ -35,12 +35,15 @@ const Image = TiptapImage.extend({ }, parseHTML() { + if (!this.options.allowBase64) { + return [{ tag: 'figure img[src]:not([src^="data:"])' }] + } + // With base64 parsing on, admit a data: URI only when its mime type is an + // image one. A data:text/html src then never becomes a node, so the parse + // rule does not have to rely on the content security policy alone. return [ - { - tag: this.options.allowBase64 - ? 'figure img[src]' - : 'figure img[src]:not([src^="data:"])', - }, + { tag: 'figure img[src]:not([src^="data:"])' }, + { tag: 'figure img[src^="data:image/"]' }, ] }, @@ -54,6 +57,10 @@ const Image = TiptapImage.extend({ return { ...this.parent?.() as ImageOptions, noLazyImages: false, + // Markdown files can legitimately contain base64 data: URI images. + // Parsing them is required, otherwise they are silently dropped on + // the next save (issue #9108). + allowBase64: true, } }, diff --git a/src/nodes/ImageInline.ts b/src/nodes/ImageInline.ts index 75dbeeea00f..4f123d0c03f 100644 --- a/src/nodes/ImageInline.ts +++ b/src/nodes/ImageInline.ts @@ -35,12 +35,15 @@ const ImageInline = TiptapImage.extend({ }, parseHTML() { + if (!this.options.allowBase64) { + return [{ tag: 'img[src]:not([src^="data:"])' }] + } + // With base64 parsing on, admit a data: URI only when its mime type is an + // image one. A data:text/html src then never becomes a node, so the parse + // rule does not have to rely on the content security policy alone. return [ - { - tag: this.options.allowBase64 - ? 'img[src]' - : 'img[src]:not([src^="data:"])', - }, + { tag: 'img[src]:not([src^="data:"])' }, + { tag: 'img[src^="data:image/"]' }, ] }, @@ -49,6 +52,9 @@ const ImageInline = TiptapImage.extend({ ...this.parent?.() as ImageOptions, noLazyImages: false, inline: true, + // See Image.ts: data: URI images must survive an edit round-trip + // instead of being dropped on save (issue #9108). + allowBase64: true, } }, diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index 2f5bb9e5011..614bcd69bdf 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -38,6 +38,22 @@ describe('Markdown though editor', () => { expect(markdownThroughEditor('~~Test~~')).toBe('~~Test~~') expect(markdownThroughEditor('Have an `inline code` element')).toBe('Have an `inline code` element') }) + test('images with data: URI survive a round-trip (#9108)', ({ markdownThroughEditor }) => { + const dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' + // standalone image (block level, wrapped in a figure) + expect(markdownThroughEditor(`![pixel](${dataUri})`)).toBe(`![pixel](${dataUri})`) + // inline image inside a paragraph + expect(markdownThroughEditor(`Before ![pixel](${dataUri}) after`)).toBe(`Before ![pixel](${dataUri}) after`) + }) + test('a non image data: URI does not become an image node (#9108)', ({ markdownThroughEditor }) => { + // allowBase64 on its own admits any mime type. The parse rules narrow it to + // data:image/, so the syntax stays literal text instead of turning into an + // image whose src could never render. The user's characters are kept either + // way, which is the point of #9108. + const htmlUri = 'data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==' + expect(markdownThroughEditor(`![x](${htmlUri})`)).toBe(`\\![x](${htmlUri})`) + expect(markdownThroughEditor(`Before ![x](${htmlUri}) after`)).toBe(`Before \\![x](${htmlUri}) after`) + }) test('ul', ({ markdownThroughEditor }) => { expect(markdownThroughEditor('+ foo\n+ bar')).toBe('+ foo\n+ bar') expect(markdownThroughEditor('* foo\n* bar')).toBe('* foo\n* bar')