fix(core): preserve cells when structurally editing ragged GFM tables#127
Open
changdapeng wants to merge 1 commit into
Open
fix(core): preserve cells when structurally editing ragged GFM tables#127changdapeng wants to merge 1 commit into
changdapeng wants to merge 1 commit into
Conversation
GFM lets table rows omit their leading/trailing pipe, and remark renders
such ragged tables as normal table widgets. The structural edits (column
drag reorder, column delete, range clear) parsed rows with
split("|") + slice/filter, which assumes canonical pipes: on ragged rows
they silently dropped the first/last cell and skipped rows whose parsed
cell count fell short of the drag index, reordering data rows while the
header stayed put — corrupting the table.
Extract the row parsing/serialization into pure functions
(table-markdown.ts) that strip at most one pipe per edge, honor \|
escapes, pad every row to the table's widest row before editing, and
re-serialize touched rows in canonical form. Well-formed tables
round-trip byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoRNuUB2DWz7csbpNvABnz
Author
|
Before/after of the reproduction table (real browser, real mouse drag of the 年龄 grip onto 职位): Before — ragged header (missing leading pipe) + trailing empty column, rendered as a 4-column widget: After — columns swapped with every cell preserved; all rows normalized with leading/trailing pipes; the adjacent well-formed table byte-identical: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary / 摘要
Structural table edits (column drag-reorder, column delete, Delete-key range clear) dropped cells and misaligned the header on GFM tables whose rows omit the leading/trailing pipe; rows are now parsed GFM-consistently and touched rows are re-serialized in canonical form.
Motivation / 背景与动机
GFM allows table rows to omit their leading and/or trailing pipe, and remark renders such "ragged" tables as normal table widgets. But
moveColumn/deleteColumn/ the range clear parsed rows withsplit("|") + slice/filter, which assumes canonical pipes:moveColumnadditionally skipped rows whose parsed cell count fell short of the drag index — data rows were reordered while the header stayed put, corrupting the table.Minimal reproduction (drag the 年龄/职位 grips to swap the two columns):
Changes / 变更内容
packages/core:src/table-markdown.ts— pure GFM table-source transforms:splitRowCells(strips at most one pipe per edge, honors\|escapes via a character scan),serializeRowCells(canonical leading+trailing-pipe form; no cell-content substitution, so pre-existing zero-width cells round-trip byte-identically),moveTableColumn/deleteTableColumn/appendTableColumn/clearTableCellRange, andtableColumnCount. Two deliberate semantics, documented in the module: the delimiter row is identified by POSITION (always the block's second line, per the GFM spec) — a dash-only body row like| - | - |is data and must never be treated as a delimiter — and the column count is the max over all rows, matching the widget's own max-colCount rendering intoDOM()(cells beyond the delimiter count are preserved, not dropped). Every row is padded to that width before editing, which removes the silent row-skip; out-of-range/negative indices return the source unchanged; alignment markers move with their column. Pure functions, no DOM/state, unit-testable in isolation.src/live-preview-table.ts—moveColumn/deleteColumn/addColumn/ the Delete-key range clear delegate to those functions (addColumn's old regex append misplaced the new cell on ragged rows and was escape-blind);addRowsizes the new row bytableColumnCountinstead of the header's AST cell count; separator checks go throughisSeparatorLine(the regex is module-private now). The widget interaction state machine (editing locks, rAF polling, drag lifecycle, dispatch ordering) is untouched.packages/core/test/table-markdown.test.ts— 39 cases: the test(core): add table drag eval harness #102 corpus ported as conservation assertions (row count, uniform column count, cell-multiset preservation, normalized pipes), alignment-marker follow, escaped pipes, out-of-range/negative-index no-op guards, byte-identical well-formed controls — including pre-existing zero-width cells (| a || c |), which round-trip untouched when a different column is edited — and dash-only body rows (| - |), which are treated as data positionally, never as a delimiter.packages/core/test/live-preview-regressions.test.ts— 3 widget-level regressions driving the real grip-click + Delete event chain; assertions readeditor.getDocument()only, no layout/coordinate dependency (jsdom reports zero layout, so coordinate-driven tests would be unreliable).Testing / 测试
pnpm testpasses — 557/557 (was 515 on main + 42 new)pnpm buildfor @floatboat/nexus-core)pnpm typecheck(pnpm -r exec tsc --noEmit) — zero errors;git diff --checkcleanCompliance / 合规自检
AI-assisted notes / AI 使用说明: The functional code in this PR was primarily developed by AI (Claude Code) under my direction; I reviewed it and take responsibility for every line.
Checklist / 自检清单
live-preview-table.ts→ walked through the 12 Table Widget rules: the diff is confined to three method bodies' string transforms + theSEPARATOR_REimport; no event chain / editing lock / rAF / drag-lifecycle code touched (rules 1, 8, 10, 11, 12 domains all untouched)Known limitation (out of scope)
The wide-table corpus case (viewport narrower than the table; the drop target can't be reached while dragging) is a layout-dependent UX issue, not a source-transform defect — it can't be covered by stable DOM-only tests and is left as a follow-up.
Relationship to the stale-offsets PR
Textually this PR and the stale-offsets PR edit the same structural-edit method bodies, so whichever lands second needs a mechanical rebase: the union is simply passing the other PR's live source (
this.currentTableSource()) as the transform input, e.g.this.dispatch(deleteTableColumn(this.currentTableSource(), colIdx)). The fixes are logically independent.