fix: emit no blank lines for empty text blocks - #3264
Conversation
🦋 Changeset detectedLatest commit: 4a09299 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bundle Stats✅ No significant changes. All scenario measurements (7)🗺️
Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time. |
1feb317 to
262d47f
Compare
262d47f to
35cc6d6
Compare
35cc6d6 to
07a7491
Compare
07a7491 to
4736fac
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
4736fac to
e80f255
Compare
`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.
02b26e3 to
4a09299
Compare

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 inportableTextToMarkdown's output:[h1 'foo', empty block, 'bar']now serializes to# foo\n\nbarinstead of# foo\n\n\n\nbar. A dropped block never survived reparsing anyway, and a customblockSpacingcallback 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
- - subform 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
portableTextToMarkdownno 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 beforeblockSpacingruns, 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.