Skip to content

fix(markdown): never parse to an empty document (fixes setContent crash) - #8017

Open
codewithsupra wants to merge 10 commits into
ueberdosis:mainfrom
codewithsupra:fix/markdown-whitespace-empty-doc
Open

fix(markdown): never parse to an empty document (fixes setContent crash)#8017
codewithsupra wants to merge 10 commits into
ueberdosis:mainfrom
codewithsupra:fix/markdown-whitespace-empty-doc

Conversation

@codewithsupra

@codewithsupra codewithsupra commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes

Fixes #7914

Changes and Review

  • parse() could return a doc with no valid block content for certain inputs, which crashes setContent.
  • Fallback now checks for actual block content rather than a non-empty array, so a custom extension returning a bare inline node can't slip through.
  • Addressed two rounds of maintainer review feedback; regression tests cover both the original bug and the review findings.

Checklist

  • I have added a changeset if necessary.
  • I have added tests if possible.
  • I have made sure to test my changes myself.

Responsibility

  • I have reviewed and understand these changes, and I take responsibility for this PR, even if an AI agent created it.

Fixes ueberdosis#7914

MarkdownManager.parse() could return { type: 'doc', content: [] } for
markdown that yields no renderable blocks — whitespace-only input, or
input whose only token has no registered handler. A common trigger:
a line of leading whitespace followed by text (e.g. '   /   ') is
tokenized by marked as an indented code block (4+ leading spaces per
CommonMark); with no code-block extension registered, that token
produces nothing, leaving empty content.

A doc node requires block+, so the empty document made setContent
throw 'RangeError: Invalid content for node doc: <>'. parse() now
falls back to a single empty paragraph when parseTokens yields no
blocks, matching how an empty markdown string is represented — so no
input string can crash the editor.

Adds regression tests (whitespace-only, whitespace+slash,
whitespace+backslash, empty string all yield a valid one-paragraph
doc; setContent no longer throws; meaningful single-char content still
parses). Verified the parse tests fail with the fix reverted. Full
markdown + core suites (713 tests) pass.
@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f8e66e4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 72 packages
Name Type
@tiptap/markdown Patch
@tiptap/core Patch
@tiptap/extension-audio Patch
@tiptap/extension-blockquote Patch
@tiptap/extension-bold Patch
@tiptap/extension-bubble-menu Patch
@tiptap/extension-bullet-list Patch
@tiptap/extension-code-block-lowlight Patch
@tiptap/extension-code-block Patch
@tiptap/extension-code Patch
@tiptap/extension-collaboration-caret Patch
@tiptap/extension-collaboration Patch
@tiptap/extension-color Patch
@tiptap/extension-details Patch
@tiptap/extension-document Patch
@tiptap/extension-drag-handle-react Patch
@tiptap/extension-drag-handle-vue-2 Patch
@tiptap/extension-drag-handle-vue-3 Patch
@tiptap/extension-drag-handle Patch
@tiptap/extension-emoji Patch
@tiptap/extension-file-handler Patch
@tiptap/extension-floating-menu Patch
@tiptap/extension-font-family Patch
@tiptap/extension-hard-break Patch
@tiptap/extension-heading Patch
@tiptap/extension-highlight Patch
@tiptap/extension-horizontal-rule Patch
@tiptap/extension-image Patch
@tiptap/extension-invisible-characters Patch
@tiptap/extension-italic Patch
@tiptap/extension-link Patch
@tiptap/extension-list Patch
@tiptap/extension-mathematics Patch
@tiptap/extension-mention Patch
@tiptap/extension-node-range Patch
@tiptap/extension-ordered-list Patch
@tiptap/extension-paragraph Patch
@tiptap/extension-strike Patch
@tiptap/extension-subscript Patch
@tiptap/extension-superscript Patch
@tiptap/extension-table-of-contents Patch
@tiptap/extension-table Patch
@tiptap/extension-text-align Patch
@tiptap/extension-text-style Patch
@tiptap/extension-text Patch
@tiptap/extension-twitch Patch
@tiptap/extension-typography Patch
@tiptap/extension-underline Patch
@tiptap/extension-unique-id Patch
@tiptap/extension-youtube Patch
@tiptap/extensions Patch
@tiptap/html Patch
@tiptap/pm Patch
@tiptap/react Patch
@tiptap/starter-kit Patch
@tiptap/static-renderer Patch
@tiptap/suggestion Patch
@tiptap/vue-2 Patch
@tiptap/vue-3 Patch
@tiptap/extension-character-count Patch
@tiptap/extension-dropcursor Patch
@tiptap/extension-focus Patch
@tiptap/extension-gapcursor Patch
@tiptap/extension-history Patch
@tiptap/extension-list-item Patch
@tiptap/extension-list-keymap Patch
@tiptap/extension-placeholder Patch
@tiptap/extension-table-cell Patch
@tiptap/extension-table-header Patch
@tiptap/extension-table-row Patch
@tiptap/extension-task-item Patch
@tiptap/extension-task-list Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploy Preview for tiptap-embed ready!

Name Link
🔨 Latest commit f8e66e4
🔍 Latest deploy log https://app.netlify.com/projects/tiptap-embed/deploys/6a5900d15d38a10008ee1946
😎 Deploy Preview https://deploy-preview-8017--tiptap-embed.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread packages/markdown/src/MarkdownManager.ts Outdated
Comment on lines +351 to +358
// A `doc` node requires at least one block child (`block+`), so an empty
// `content` array produces an invalid document that makes `setContent`
// throw `RangeError: Invalid content for node doc: <>`. This happens for
// input that yields no renderable blocks — whitespace-only markdown, or
// markdown whose only token has no registered handler (e.g. an indented
// code block when no code-block extension is present, as with a line of
// leading whitespace followed by text). Fall back to a single empty
// paragraph, matching how an empty markdown string is represented.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this comment to just before "content", and re-write it to make it shorter.

// Convert tokens to Tiptap JSON
const content = this.parseTokens(tokens, true)

// Return a document node containing the parsed content

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not delete this comment

Co-authored-by: Arnau Gómez Farell <arnaugomez@users.noreply.github.com>
@cursor
cursor Bot requested a review from a team as a code owner July 16, 2026 10:28
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

MarkdownManager.parse() now falls back to a document containing an empty paragraph when parsing produces no non-text block nodes. Regression tests cover whitespace, slash input, custom handlers, extension registration, hardcoded headings, and Editor content updates.

Changes

Markdown empty document fallback

Layer / File(s) Summary
Parse fallback for empty top-level content
packages/markdown/src/MarkdownManager.ts
MarkdownManager.parse() replaces content containing only top-level text nodes with a document containing one empty paragraph.
Parsing and Editor regression coverage
packages/markdown/__tests__/empty-doc-fallback.spec.ts, .changeset/fix-markdown-empty-doc-fallback.md
Tests cover empty and slash-containing input, custom handlers, registered extensions, hardcoded heading parsing, Editor initialization, and setContent; the changeset records the patch release.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: area: markdown, complexity: medium, impact: medium

Suggested reviewers: bdbch

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change stops empty docs and setContent crashes for the reported markdown inputs, with tests covering the issue cases.
Out of Scope Changes check ✅ Passed The changes stay focused on the markdown parse fallback, regression tests, and changeset; no unrelated edits stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title is concise, specific, and matches the main change: preventing empty markdown documents that crash setContent.
Description check ✅ Passed The PR description follows the template and covers fixes, changes, checklist, and responsibility with issue linking.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/markdown/__tests__/empty-doc-fallback.spec.ts`:
- Around line 31-40: Update the test covering whitespace-plus-slash markdown to
initialize an empty Editor first, then invoke editor.commands.setContent with
the markdown content and contentType: 'markdown'. Keep the no-throw assertion
and paragraph JSON validation focused on this command path rather than
constructor initialization.

In `@packages/markdown/src/MarkdownManager.ts`:
- Around line 354-355: Update the document content construction in
MarkdownManager to reuse parsed content only when it contains at least one node
recognized by the registered schema as a block type, rather than checking
content.length alone. Otherwise, use the existing paragraph fallback, and add a
regression case covering an unsupported top-level token that produces text
content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c81410a3-d9d2-4fbd-b1f2-ebfcc5ad4933

📥 Commits

Reviewing files that changed from the base of the PR and between ecc0d40 and 06f37b5.

📒 Files selected for processing (3)
  • .changeset/fix-markdown-empty-doc-fallback.md
  • packages/markdown/__tests__/empty-doc-fallback.spec.ts
  • packages/markdown/src/MarkdownManager.ts

Comment thread packages/markdown/__tests__/empty-doc-fallback.spec.ts
Comment thread packages/markdown/src/MarkdownManager.ts Outdated
arnaugomez
arnaugomez previously approved these changes Jul 16, 2026
…ercise setContent command path in tests

Addresses CodeRabbit review on ueberdosis#8017:
- The empty-doc fallback checked content.length > 0, which doesn't
  guarantee the doc's block+ requirement — a custom token handler
  returning a bare top-level {type:'text'} node would pass the check
  and still crash setContent. Now checks that at least one top-level
  node is actually a registered block node type.
- Derives block-node names per-extension instead of via getSchema(),
  which requires the full extension set to form one valid ProseMirror
  schema; that's stricter than this check needs and would silently
  discard valid content if unrelated extensions conflict.
- Added a setContent-via-commands regression test alongside the
  existing constructor-init one, and a regression test/fix pair for
  the bare-text-node case using a custom extension.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/markdown/src/MarkdownManager.ts`:
- Around line 1084-1106: Update the block-node cache construction around
schemaBlockNodeNamesCache to derive names from this.extensions so directly
registered block extensions are included, while preserving the existing node and
inline/topNode filtering. In registerExtension(), invalidate
this.schemaBlockNodeNamesCache after updating this.extensions, and add a
regression test covering directly registered block content without introducing
the empty paragraph fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 460432a7-9665-4c3c-86c5-247daf57234b

📥 Commits

Reviewing files that changed from the base of the PR and between 06f37b5 and 7402393.

📒 Files selected for processing (2)
  • packages/markdown/__tests__/empty-doc-fallback.spec.ts
  • packages/markdown/src/MarkdownManager.ts

Comment thread packages/markdown/src/MarkdownManager.ts Outdated
…ension in the block-node cache

Addresses CodeRabbit review on ueberdosis#8017. schemaBlockNodeNamesCache only
read this.baseExtensions (the constructor's initial list), so a block
extension added later via the public registerExtension() method would
have its content wrongly replaced by the empty-paragraph fallback.
Read this.extensions instead (every extension ever registered), and
invalidate the cache whenever registerExtension() runs.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 16, 2026
content.some(node => ...) yields node.type as string | undefined
(JSONContent.type is optional); Set<string>.has() requires a string
argument. CI's dts build (stricter than local tsc --noEmit here) caught
what plain vitest runs didn't.
* set), and treating that failure as "no block nodes exist" would discard
* otherwise-correct parsed content.
*/
private getSchemaBlockNodeNames(): Set<string> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this function check for the group: "block" property of the Node extension?

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 16, 2026
…text' test

The extension/schema-derived block-node check broke on a case I found
by simulating CI's merge of this branch with the latest main: the
'heading' case in parseToken's switch statement builds a
{ type: 'heading' } node directly, independent of whether a Heading
extension is registered. A manager built with only
[Document, Paragraph, Text] correctly returns a heading node for a
setext heading — but the extension-derived block-name set had no
entry for 'heading' (no such extension was registered), so the
fallback wrongly discarded it and replaced it with an empty paragraph.

The actual invariant only needs to rule out one concrete shape: a bare
top-level `text` node, which is the only inline type parseToken ever
returns unwrapped (the 'text' and 'escape' cases). Checking that
directly removes the whole extension/schema-introspection path
(getSchemaBlockNodeNames, its cache, and the registerExtension
invalidation) along with its failure modes, and is correct for every
case already covered: the original whitespace/slash bug, a custom
handler returning bare text, a directly-registered block extension,
and now a hardcoded-case node type with no matching extension.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 16, 2026
arnaugomez
arnaugomez previously approved these changes Jul 16, 2026
@arnaugomez

Copy link
Copy Markdown
Contributor

I added these fixes:

  • Use the "group" property to check if a node is in the "block" group. This is the way to know if a node is a valid children of the doc node.
  • Fix failing test, because the "Heading" extension was not registered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown: setContent issues when content only contains whitespace and slashes

3 participants