Skip to content

fix: emit no blank lines for empty text blocks - #3264

Merged
christianhg merged 1 commit into
mainfrom
markdown-skip-empty-block-output
Sep 11, 2026
Merged

fix: emit no blank lines for empty text blocks#3264
christianhg merged 1 commit into
mainfrom
markdown-skip-empty-block-output

Conversation

@christianhg

@christianhg christianhg commented Sep 10, 2026

Copy link
Copy Markdown
Member

A block that renders to the empty string (an empty or whitespace-only text block, or a custom renderer returning '') no longer leaves blank lines in portableTextToMarkdown's output: [h1 'foo', empty block, 'bar'] now serializes to # foo\n\nbar instead of # foo\n\n\n\nbar. A dropped block never survived reparsing anyway, and a custom blockSpacing callback now sees the pair of blocks that actually end up adjacent, never an invisible one. Two spacing consequences show in rendered HTML: two blockquotes separated only by an empty block now join into one quote with a paragraph break, and a list whose blank lines came only from empty blocks goes tight, since a skipped block no longer counts toward looseness.

The same filter runs inside containers: callout and structured-blockquote content joins skip empty blocks, so no more blank quote-prefixed lines. In list items, the marker line goes to the first block that renders output, with two exceptions that keep the markdown reparseable: a multi-line block (a code fence, a table) keeps its later lines indented inside the item instead of escaping the list at column 0, and a nested list or, on a task item, any non-text block stays indented below a bare marker, because after - [x] (or fused with - ) it would reparse as plain words.

The marker-line rules also close two pre-existing escapes that predate the filter: a multi-line block glued raw to the marker line used to end the list at the first column-0 line, and a task item's checkbox line could carry a fence whose reparse turned it into item text. For bullet and number items the fused - - sub form actually reparses the same as the bare-marker form; the bare marker is the canonical shape, and the hard rule exists for task items, where fusing destroys the sublist. Code fences are untouched: blank lines inside a fence are content.


Note

Medium Risk
Changes core serialization and list/blockquote assembly in @portabletext/markdown, which can alter markdown spacing and HTML rendering for documents with empty blocks, though behavior is aligned with reparse expectations and heavily tested.

Overview
portableTextToMarkdown no longer emits extra blank lines for blocks that render to nothing (empty/whitespace-only text, or custom type renderers returning ''). Those blocks are filtered out before blockSpacing runs, so spacing callbacks only see neighbors that actually appear in the output.

The same drop happens when joining callout and structured blockquote inner content, avoiding stray > blank lines. Structural lists were updated so empty inner blocks do not force loose spacing or wrong marker placement: the list marker attaches to the first block with output, with reparse-safe rules for nested lists, task checkboxes, and multi-line blocks (e.g. code fences stay indented under the item). Extensive round-trip tests cover these cases.

Reviewed by Cursor Bugbot for commit 4a09299. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4a09299

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

This PR includes changesets to release 1 package
Name Type
@portabletext/markdown 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

@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
portable-text-editor-documentation Ready Ready Preview Sep 11, 2026 6:50am UTC
portable-text-example-basic Ready Ready Preview Sep 11, 2026 6:50am UTC
portable-text-playground Ready Ready Preview Sep 11, 2026 6:50am UTC

Request Review

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Bundle Stats

✅ No significant changes.

All scenario measurements (7)

🗺️ @portabletext/editor / @portabletext/editor · @portabletext/editor / @portabletext/editor/behaviors · @portabletext/editor / @portabletext/editor/plugins · @portabletext/editor / @portabletext/editor/selectors · @portabletext/editor / @portabletext/editor/traversal · @portabletext/editor / @portabletext/editor/utils · @portabletext/markdown / @portabletext/markdown · Artifacts

Scenario Kind Bundle (raw / gzip) Gzip change Import time Import change
⚪ @portabletext/editor / @portabletext/editor export 1.09 MB / 254.3 KB None 69 ms +1 ms, +2.1%
⚪ @portabletext/editor / @portabletext/editor/behaviors export 4.0 KB / 1.4 KB None 2 ms +0 ms, +0.2%
⚪ @portabletext/editor / @portabletext/editor/plugins export 5.1 KB / 1.8 KB None 7 ms +0 ms, +1.7%
⚪ @portabletext/editor / @portabletext/editor/selectors export 94.7 KB / 21.7 KB None 8 ms +0 ms, +1.9%
⚪ @portabletext/editor / @portabletext/editor/traversal export 42.8 KB / 11.2 KB None 6 ms -0 ms, -2.9%
⚪ @portabletext/editor / @portabletext/editor/utils export 33.8 KB / 9.1 KB None 6 ms +0 ms, +2.8%
⚪ @portabletext/markdown / @portabletext/markdown export 314.4 KB / 91.2 KB +239 B, +0.3% 44 ms +2 ms, +3.6%

Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/markdown/src/from-portable-text/renderers/type.ts Outdated
Comment thread packages/markdown/src/from-portable-text/renderers/type.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/markdown/src/from-portable-text/renderers/type.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4736fac. Configure here.

Comment thread packages/markdown/src/from-portable-text/renderers/type.ts Outdated
`portableTextToMarkdown` joined every block's rendered output with
block spacing, so a block rendering to the empty string (an empty or
whitespace-only text block, or a custom renderer returning `''`) left
its separators behind: `[h1 'foo', empty block, 'bar']` serialized to
`# foo\n\n\n\nbar`. The blank lines mean nothing anywhere downstream,
rendered HTML collapses them and the parser folds any blank run back
into one block separator, so they only wasted tokens and misled
markdown editors into treating spacing as editable.

Blocks now render first, empties are filtered by rendered output (not
block shape, which is what covers the `''`-returning custom renderer),
and the join runs over the survivors, with `blockSpacing` called
between the neighbors that actually end up adjacent. Rendering keeps
each block's original index, since `renderNode` uses it for list
numbering context. The same filter runs at every content join that
stacks separators: callout and structured-blockquote content
(quote-prefixed blank lines) and list-item content. Two spacing
consequences are visible in rendered HTML and disclosed in the
changeset: adjacent surviving blockquotes join into one quote, and
lists loose only through empty blocks go tight, since looseness is
counted from rendered non-empty content.

Inside a list item the marker line goes to the first block that
renders output, with the first-block mark (line-start hazard escaping)
placed on every candidate until one renders non-empty, and two
exceptions that keep the markdown reparseable. A multi-line block's
later lines (a code fence's body, a table's rows) are indented as
continuation lines, or CommonMark ends the list at the first column-0
line (pre-existing: a multi-line block as an item's first content
escaped the item the same way before this change). A nested list, or
on a task item any non-text block, stays indented below a bare marker:
after `- [x] ` everything is inline text, so a fence, table, or
`json:object` payload promoted there reparses as words (for bullet and
number items the fused `- - sub` form reparses identically to the bare
marker; the bare marker is canonicalization there and a data fix only
for tasks). The empty-item trim runs on the joined head so it reaches
only the last line, leaving a hard break's trailing spaces on earlier
lines intact.

Reparse behavior for the empty-block removal itself is unchanged,
pinned by round trips; the marker-line pins were each proven red on
the prior join, and the task and nested-list shapes are pinned with
full round trips of both markdown and reparsed Portable Text.
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.

1 participant