Skip to content

fix: honor table headerRows: 0 in Markdown serialization#2887

Merged
christianhg merged 1 commit into
mainfrom
fix-markdown-table-header-rows
Jul 1, 2026
Merged

fix: honor table headerRows: 0 in Markdown serialization#2887
christianhg merged 1 commit into
mainfrom
fix-markdown-table-header-rows

Conversation

@christianhg

Copy link
Copy Markdown
Member

portableTextToMarkdown ignored a table's headerRows field and always promoted rows[0] to the GFM header. A headerless table (headerRows: 0) lost that fact on export, rows[0] was reinterpreted as the header, and the round-trip back through markdownToPortableText returned headerRows: 1.

pt→md (DefaultTableRenderer): headerRows === 0 now emits an empty header row (padded to the column count) plus the delimiter, with every row in the body. Any other value (undefined or >= 1) still promotes rows[0]. GFM allows a single header row, so header rows beyond the first flatten into the body (lossy, and the extra rows stay on the Portable Text side).

md→pt: an all-empty header row is dropped and recorded as an explicit headerRows: 0 (distinct from the pre-existing no-header undefined fallback); a non-empty header stays headerRows: 1. Markdown only ever emits 0 or 1, so a headerless table round-trips cleanly with no phantom empty header row.

Round-trips: 0 → empty-header md → 0; 1 → header md → 1; >= 2 → single-header md → 1 (lossy on export, > 1 preserved in the PT source, documented).

headerRows stays number, so this is a serialization-behavior change, not an API break. The existing portable-text-to-markdown "headerRows: 0 promotes the first row" test flips (now asserts the empty header + full body), and a new markdown-to-portable-text test pins the empty-header read-back to headerRows: 0 with only the body rows (full-literal toEqual, deterministic keys). All 292 markdown-package tests pass; types/lint/build/knip/format green.

Out of scope (follow-up): the ticket notes @portabletext/plugin-table's header feature should adopt the same headerRows: number. Happy to file that separately.

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eedc411

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

This PR includes changesets to release 15 packages
Name Type
@portabletext/markdown Patch
@portabletext/editor Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-dnd Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-list-index Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-table Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar 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 Jul 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment Jul 1, 2026 11:35am
portable-text-example-basic Ready Ready Preview, Comment Jul 1, 2026 11:35am
portable-text-playground Ready Ready Preview, Comment Jul 1, 2026 11:35am

Request Review

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (0b82b78e)

@portabletext/editor

Metric Value vs main (0b82b78)
Internal (raw) 792.8 KB -
Internal (gzip) 151.5 KB -
Bundled (raw) 1.40 MB +544 B, +0.0%
Bundled (gzip) 315.7 KB +151 B, +0.0%
Import time 97ms -2ms, -2.3%

@portabletext/editor/behaviors

Metric Value vs main (0b82b78)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -2.6%

@portabletext/editor/plugins

Metric Value vs main (0b82b78)
Internal (raw) 2.7 KB -
Internal (gzip) 894 B -
Bundled (raw) 2.5 KB -
Bundled (gzip) 827 B -
Import time 7ms -0ms, -2.2%

@portabletext/editor/selectors

Metric Value vs main (0b82b78)
Internal (raw) 79.7 KB -
Internal (gzip) 14.6 KB -
Bundled (raw) 75.2 KB -
Bundled (gzip) 13.5 KB -
Import time 8ms -0ms, -1.9%

@portabletext/editor/traversal

Metric Value vs main (0b82b78)
Internal (raw) 26.2 KB -
Internal (gzip) 5.2 KB -
Bundled (raw) 26.1 KB -
Bundled (gzip) 5.1 KB -
Import time 6ms -0ms, -1.8%

@portabletext/editor/utils

Metric Value vs main (0b82b78)
Internal (raw) 29.7 KB -
Internal (gzip) 6.2 KB -
Bundled (raw) 27.3 KB -
Bundled (gzip) 5.8 KB -
Import time 6ms -0ms, -2.2%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @portabletext/markdown

Compared against main (0b82b78e)

Metric Value vs main (0b82b78)
Internal (raw) 53.8 KB +762 B, +1.4%
Internal (gzip) 9.8 KB +198 B, +2.0%
Bundled (raw) 348.9 KB +767 B, +0.2%
Bundled (gzip) 96.3 KB +195 B, +0.2%
Import time 40ms -1ms, -3.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

`portableTextToMarkdown` ignored a table's `headerRows` and always
promoted `rows[0]` to the GFM header, so a headerless table
(`headerRows: 0`) lost that fact on export: `rows[0]` was reinterpreted
as a header and the round-trip back through `markdownToPortableText`
returned `headerRows: 1`.

pt->md (`DefaultTableRenderer`): `headerRows === 0` now emits an empty
header row (padded to the column count) plus the delimiter, with every
row in the body. Any other value (`undefined` or `>= 1`) still promotes
`rows[0]`; GFM's single header row means header rows beyond the first
flatten into the body (lossy, preserved on the PT side).

md->pt: an all-empty header row is dropped and recorded as
`headerRows: 0` (an explicit 0, distinct from the no-table-header
`undefined` fallback); a non-empty header stays `headerRows: 1`. Markdown
only ever emits 0 or 1, so a headerless table round-trips cleanly and no
phantom empty header row is introduced.

`headerRows` stays `number`, so this is a serialization-behavior change,
not an API break: the existing "`headerRows: 0` promotes the first row"
test flips, and a `markdownToPortableText` test pins the empty-header
read-back.
@christianhg christianhg force-pushed the fix-markdown-table-header-rows branch from df333f1 to eedc411 Compare July 1, 2026 11:33
@christianhg christianhg marked this pull request as ready for review July 1, 2026 11:37
@christianhg christianhg merged commit a570ace into main Jul 1, 2026
17 checks passed
@christianhg christianhg deleted the fix-markdown-table-header-rows branch July 1, 2026 11:41
@ecoscript ecoscript Bot mentioned this pull request Jul 1, 2026
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