Skip to content

@tiptap/markdown: HTML comment inside a heading crashes with RangeError: Invalid content for node type heading #8116

Description

@JustinTulloss

Affected Packages

@tiptap/markdown

Tiptap Version

3.28.0

Browser Used

Chrome

What happened?

Parsing markdown that contains an inline HTML comment inside a heading crashes the editor:

RangeError: Invalid content for node type heading
    at setNodeMarkup
    at appendTransaction   (@tiptap/extension-table-of-contents)

Steps to reproduce

Load this markdown into an editor built from StarterKit + @tiptap/extension-table-of-contents, in a browser (real DOM present):

## Week of <!-- date -->

The document loads, then the table-of-contents plugin throws on the next transaction.

Root cause

  1. parseHTMLToken in MarkdownManager routes unrecognized HTML through generateJSON (a real ProseMirror DOM parse). This path is only taken when window.DOMParser exists — headless, it already falls back to literal text, so the bug is browser-only.
  2. generateJSON("<!-- date -->") has no element to keep, so it returns an empty paragraph: { type: "doc", content: [{ type: "paragraph" }] }.
  3. For an inline comment token, that empty block paragraph gets spliced into the heading's inline content — an invalid document that nonetheless loads.
  4. @tiptap/extension-table-of-contents calls setNodeMarkup on every heading on every transaction (to stamp data-toc-id), which re-validates the node → RangeError. A comment in a heading is the worst case because the TOC plugin touches every heading constantly.

The unrecognized-HTML guard added in #7916 keys off tag names via /<\/?([a-zA-Z][\w-]*)/, which never matches <!--. So a comment is treated as recognized HTML and still hits generateJSON, instead of being sent through the existing htmlAsLiteralText fallback.

Suggested fix

In MarkdownManager.parseHTMLToken, detect comment-only markup before the DOM parse and fall back to literal text:

     if (!html.trim()) {
       return null;
     }
+    // A comment-only token has no element for the DOM parser to keep, so
+    // generateJSON reduces it to an empty paragraph; returning that block node
+    // into inline content crashes on re-validation. Comments carry no tag name,
+    // so isUnrecognizedHtml misses them. Pass comment-only markup through as text.
+    if (/^\s*(?:<!--[\s\S]*?-->\s*)+$/.test(html)) {
+      return this.htmlAsLiteralText(html, !!token.block);
+    }
     if (this.isUnrecognizedHtml(html)) {
       return this.htmlAsLiteralText(html, !!token.block);
     }

Detecting the comment before the parse (rather than checking for an empty paragraph after) matters: <!-- x --> and a genuine empty <p></p> both reduce to an empty paragraph, so a post-parse check would wrongly convert real empty <p></p> blocks to literal text too. The up-front check is comment-specific.

Environment

  • @tiptap/markdown@3.28.0
  • Browser (bug does not reproduce headless / without window.DOMParser)
  • With @tiptap/extension-table-of-contents (any extension that runs setNodeMarkup on headings triggers it; TOC is just the most common)

Expected Behavior

A comment-only token should be passed through via htmlAsLiteralText (matching the headless path and CommonMark's "raw HTML passes through verbatim" semantics), not turned into an empty block node in inline content.

Reproducible Example URL (Optional)

No response

Additional Context (Optional)

No response

Metadata

Metadata

Assignees

Labels

area: editorEditor behavior, commands, and transactionsarea: markdownMarkdown parsing and serializationstatus: triageNeeds initial review and categorization

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions