diff --git a/.changeset/fix-static-renderer-namespaced-tag.md b/.changeset/fix-static-renderer-namespaced-tag.md new file mode 100644 index 0000000000..341c4814cc --- /dev/null +++ b/.changeset/fix-static-renderer-namespaced-tag.md @@ -0,0 +1,5 @@ +--- +'@tiptap/static-renderer': patch +--- + +`renderToHTMLString` now closes namespaced elements correctly. The closing tag no longer repeats the `xmlns` declaration, and tags that cannot self-close, such as `div` or `iframe`, are no longer written as self-closing. diff --git a/packages/static-renderer/__tests__/json-string.spec.ts b/packages/static-renderer/__tests__/json-string.spec.ts index e8e99993b9..53bcad42b8 100644 --- a/packages/static-renderer/__tests__/json-string.spec.ts +++ b/packages/static-renderer/__tests__/json-string.spec.ts @@ -1,4 +1,4 @@ -import { extensions as coreExtensions } from '@tiptap/core' +import { extensions as coreExtensions, Node as CoreNode } from '@tiptap/core' import Bold from '@tiptap/extension-bold' import CodeBlock from '@tiptap/extension-code-block' import Document from '@tiptap/extension-document' @@ -562,3 +562,37 @@ describe('static render json to string (with prosemirror)', () => { expect(html).toMatch(/data-toc-id="[^"]+"/) }) }) + +describe('namespaced DOMOutputSpec', () => { + // ProseMirror encodes a namespaced spec as `" "`. + const Diagram = CoreNode.create({ + name: 'diagram', + group: 'block', + content: 'text*', + renderHTML: () => ['http://www.w3.org/2000/svg svg', { width: '100' }, 0], + }) + + it('keeps the xmlns attribute out of the closing tag', () => { + const html = renderToHTMLString({ + content: { + type: 'doc', + content: [{ type: 'diagram', content: [{ type: 'text', text: 'hi' }] }], + }, + extensions: [Document, Paragraph, Text, Diagram], + }) + + // The namespace belongs on the opening tag only. + expect(html).toContain('$/) + }) + + it('still resolves non-self-closing tags by their local name', () => { + // `div` is in NON_SELF_CLOSING_TAGS, so the namespaced spec must not self-close. + const html = domOutputSpecToHTMLString([ + 'http://www.w3.org/1999/xhtml div', + { class: 'wrapper' }, + ])() + + expect(html).toContain('> 1) { - tag = `${parts[1]} xmlns="${parts[0]}"` - } + // ProseMirror encodes a namespaced spec as `" "`. + // Only the opening tag carries the `xmlns` attribute: the closing tag and the + // `NON_SELF_CLOSING_TAGS` lookup below both need the bare local name. + const parts = _tag.split(' ') + const tag = parts.length > 1 ? parts[1] : _tag + const openTag = parts.length > 1 ? `${parts[1]} xmlns="${parts[0]}"` : _tag if (attrs === undefined) { - return () => `<${tag}/>` + return () => `<${openTag}/>` } if (attrs === 0) { - return child => `<${tag}>${serializeChildrenToHTMLString(child)}` + return child => `<${openTag}>${serializeChildrenToHTMLString(child)}` } if (typeof attrs === 'object') { if (Array.isArray(attrs)) { if (children === undefined) { return child => - `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}` + `<${openTag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}` } if (children === 0) { return child => - `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}` + `<${openTag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}` } return child => - `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}${[children] + `<${openTag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}${[children] .concat(rest) .map(a => domOutputSpecToHTMLString(a)(child))}` } if (children === undefined) { if (NON_SELF_CLOSING_TAGS.has(tag)) { - return () => `<${tag}${serializeAttrsToHTMLString(attrs)}>` + return () => `<${openTag}${serializeAttrsToHTMLString(attrs)}>` } - return () => `<${tag}${serializeAttrsToHTMLString(attrs)}/>` + return () => `<${openTag}${serializeAttrsToHTMLString(attrs)}/>` } if (children === 0) { return child => - `<${tag}${serializeAttrsToHTMLString(attrs)}>${serializeChildrenToHTMLString(child)}` + `<${openTag}${serializeAttrsToHTMLString(attrs)}>${serializeChildrenToHTMLString(child)}` } return child => - `<${tag}${serializeAttrsToHTMLString(attrs)}>${[children] + `<${openTag}${serializeAttrsToHTMLString(attrs)}>${[children] .concat(rest) .map(a => domOutputSpecToHTMLString(a)(child)) .join('')}`