Repository navigation
Conversation
…ine breaks in Markdown table cells as <br> ## Description A GFM table cell holds a single line, so line breaks in a cell need the `<br>` convention that GitHub and most renderers accept there. - The playground's `TABLE` Markdown transformer wrote a literal `\n` for each newline in a cell (`one\n\ntwo`), which no renderer reads. It now writes `<br>`, escapes `|` in cell text, reads `<br>`, `<br/>` and `<br />` back (the legacy `\n` too), splits rows only on unescaped pipes, and trims cell padding on import. - `@lexical/mdast`'s `MdastTableExtension` exported a cell's paragraphs and line breaks as spaces, so the lines were lost, and fused the items of a list in a cell (`xy`). It now writes each paragraph, line break, list item and code line as its own `<br>`-separated line, and imports `<br>` as a paragraph boundary (nested in formatting, as a line break). Without this, `<br>` imported as literal text and exported as `\<br>`. - Column alignment in `@lexical/mdast` was stored as one array on the table, so deleting or inserting a column shifted every alignment after it. It is now stored on each cell of the column; the old table-level state is still read for existing documents. - The mdast-editor dev example gets table editing: `TableExtension` (with the features GFM can't express turned off), Insert > Table, a Table menu for adding and removing rows and columns, table styles, and a Tables section in the demo document. Closes facebook#9323 ## Test plan ### Before ``` $ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts × writes line breaks in a cell as <br> and escapes pipes × reads <br>, escaped pipes and the legacy \n back × keeps each column its alignment when a column is removed × gives an inserted column no alignment × joins multi-paragraph cells instead of fusing their text × writes line breaks in a cell as <br>, never a newline × reads <br> in a cell as a paragraph boundary AssertionError: expected '| a | b |\n| --- | --- |\n| one\n\ntw…' to be '| a | b |\n| --- | --- |\n| one<br>tw…' AssertionError: expected '| b |\n| :- |\n| 2 |' to be '| b |\n| -: |\n| 2 |' AssertionError: expected 'a b' to be 'a<br>b' AssertionError: expected [ 'x <br/> y<BR /><br>z<br>w' ] to deeply equal [ 'x', 'y', '', 'z\nw' ] Tests 7 failed | 103 passed (110) ``` ### After ``` $ vitest run --project unit packages/lexical-mdast packages/lexical-playground dev-examples Test Files 40 passed (40) Tests 624 passed (624) $ vitest run --project browser dev-examples packages/lexical-mdast Tests 36 passed (36) $ tsc -p . # exit 0 ``` The new browser test `dev-examples/mdast-editor/src/__tests__/browser/TableEdit.test.ts` covers the Table menu's commands and pressing Enter in a cell, which exports `| 1 | 2<br>3 |`. Browser tests ran in Chromium only; Firefox and WebKit were not exercised. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… as the table cells' format
## Description
`MdastTableExtension` kept a GFM table's column alignment (`| :-: |`) for
export but never showed it: an aligned column looked left-aligned in the
editor, and nothing could change it. Alignment is now the element format
of each cell in the column, which Lexical already renders as `text-align`
and which `FORMAT_ELEMENT_COMMAND` sets on a table cell selection. Export
reads a cell's own format, else the format all of its paragraphs share
(what that command sets with the caret in one cell), so aligning text in
the editor aligns the column in Markdown. Documents saved with the older
table-level alignment state still export it for columns whose cells are
not aligned.
The mdast-editor dev example's Table menu gains Align column
left/center/right and Clear column alignment.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts
× renders column alignment as the cells' element format
× exports alignment set on a cell or on all of its blocks
AssertionError: expected [ [ '', '', '' ], [ '', '', '' ] ] to deeply equal [ [ 'left', 'center', '' ], …(1) ]
AssertionError: expected '| a | b |\n| - | - |\n| 1 | 2 |' to be '| a | b |\n| -: | :-: |\n| 1 | 2…'
Tests 2 failed | 99 passed (101)
```
### After
```
$ vitest run --project unit packages/lexical-mdast packages/lexical-playground dev-examples
Test Files 40 passed (40)
Tests 626 passed (626)
$ vitest run --project browser dev-examples
Tests 39 passed (39)
$ tsc -p . # exit 0
```
Browser tests ran in Chromium only; Firefox and WebKit were not exercised.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lock cursor
## Description
In the mdast-editor dev example, deleting all of the document (select-all and Backspace, cut, ...) left the footnotes section behind, because the definitions live in a root slot outside the selection. A RootNode transform now removes the section once only empty paragraphs remain, except while the caret is in a definition or when the Markdown pane's sync (tagged history-merge) leaves only definitions mid-edit.
The example's theme also had no `blockCursor`, so the cursor Lexical draws between adjacent tables or decorators was invisible. It is now a blinking bar, styled with Tailwind classes plus a keyframe in styles.css.
## Test plan
### Before
```
$ vitest run --project browser dev-examples
× deleting everything else clears the footnotes
× the toolbar survives select-all and Backspace with footnotes "\n\n[^a]: Footnote"
AssertionError: expected <div class="footnote-def" …(3)>…(2)</div> to be null
AssertionError: expected '\n\n[^a]: Footnote' to be '' // Object.is equality
Tests 2 failed | 40 passed (42)
```
### After
```
$ vitest run --project browser dev-examples
Tests 42 passed (42)
```
The block cursor was checked by screenshot in Chromium with the selection between two tables. Browser tests ran in Chromium only; Firefox and WebKit were not exercised.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
## Description
`$alignSelectedColumns` read `selection.anchor` on a `BaseSelection`, which the example's own `tsc && vite build` rejects (the root `tsc` doesn't check dev examples), so the prepare-release integration test failed on `dev-examples/mdast-editor`. It now reads the anchor only from a `RangeSelection`.
## Test plan
### Before
```
$ cd dev-examples/mdast-editor && tsc -p .
src/extensions/TableEditExtension.ts(66,45): error TS2339: Property 'anchor' does not exist on type 'BaseSelection'.
```
### After
```
$ cd dev-examples/mdast-editor && tsc -p . # exit 0
$ pnpm run build && (cd dev-examples/mdast-editor && pnpm run build) # exit 0
$ vitest run --project browser dev-examples/mdast-editor/src/__tests__/browser/TableEdit.test.ts
Tests 13 passed (13)
```
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Description The root `tsc` doesn't include `dev-examples/*`, so a type error there only surfaced in the prepare-release integration test, which runs each example's `tsc && vite build` (as happened with `TableEditExtension.ts` in e8032ff). Each dev example now has a `tsc` script, and a root `tsc-dev-examples` script runs them all; `ci-check` runs it alongside the other type checks. AGENTS.md lists the new script. ## Test plan ### Before ``` $ pnpm run ci-check # with e8032ff's fix reverted: dev-examples are not type-checked, so it passes ``` ### After ``` $ pnpm run tsc-dev-examples # with e8032ff's fix reverted dev-examples/mdast-editor tsc: src/extensions/TableEditExtension.ts(66,45): error TS2339: Property 'anchor' does not exist on type 'BaseSelection'. dev-examples/mdast-editor tsc: Failed $ pnpm run tsc-dev-examples # on this branch dev-examples/hmr tsc: Done dev-examples/dom-import tsc: Done dev-examples/mdast-editor tsc: Done ``` Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rayValue
## Description
`MdastTableExtension` coerced alignments with a hand-written `parseAlign` and parsed the legacy `mdastTableAlign` state with an ad-hoc array map. Both now use the `lexical` serialization schemas: `enumValue([null, 'left', 'center', 'right'])` for one alignment (reused to read a cell's or paragraph's format) and `arrayValue` of it for the table state. Parsing behaves the same: unknown values read as `null`, and a non-array state as `[]`.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-mdast
Tests 213 passed (213)
```
### After
```
$ vitest run --project unit packages/lexical-mdast
Tests 213 passed (213)
$ tsc -p . # exit 0
```
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rites
## Description
`MdastTableExtension` rewrote `<br>` to `break` on import and `break` to `<br>` on export with two hand-written recursive loops, and built tables and joined cell lines with push loops. Both rewrites now share one `mapPhrasing` helper, cell lines are joined by one `joinLines` helper used for text newlines and cell export alike, and import builds the table with nested `map`s. Behavior is unchanged.
## Test plan
### Before
`vitest run --project unit packages/lexical-mdast`: Tests 213 passed (213)
### After
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-mdast
Tests 213 passed (213)
```
eslint, prettier and `tsc -p .` are clean.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e cases from issue 9323
## Description
Fixes four problems reported on issue 9323 in the playground's Markdown table transformer:
- A hard line break remembers its Markdown marker (`\` or two spaces), even after a paste, and the cell export put it in front of the `<br>`, so `a\<br>b` read back as an escaped `<`. Cells now export line breaks through a text match transformer that writes a bare `<br>`.
- Reading the legacy literal `\n` also matched an escaped backslash followed by `n`, so `C:\\new` came back as `C:\` and a line break. Escaped backslashes are now skipped.
- A single-column table with a header row exported no delimiter row, because its only header cell is ROW|COLUMN and the check compared against ROW exactly. It now uses `hasHeaderState`.
- `| --- |` with no table above it was swallowed into an empty paragraph. The transformer now cancels, and `@lexical/markdown` restores the line's text when an element transformer cancels its import (it used to keep the line with the matched prefix cut off).
## Test plan
### Before
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts
× writes a hard line break marked with a backslash as a bare <br>
× writes the delimiter row of a single-column table
× keeps an escaped backslash before n when reading the legacy \n
× leaves a delimiter row with no table above it as text
Tests 4 failed | 11 passed (15)
- | a<br>b |
+ | a\<br>b |
- "C:\\new",
+ "C:
+ ew",
$ ./node_modules/.bin/vitest run --project unit packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts -t "cancels its import"
AssertionError: expected 'kept' to be '# kept' // Object.is equality
```
### After
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-markdown packages/lexical-playground packages/lexical-mdast
Tests 1222 passed (1222)
```
eslint, prettier and `tsc -p .` are clean.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… as one line of HTML
## Description
A GFM table cell holds one line of inline content, so `MdastTableExtension` flattened every block in a cell into lines: lists lost their markers and nesting, headings their level, and code blocks their language. GFM allows inline HTML in a cell, so when the editor also has `MdastHtmlExtension` (declared as a peer dependency and looked up with `$getPeerDependency`), lists, task lists, headings, quotes, code blocks and rules in a cell are now written as one line of HTML around their Markdown content, such as `intro<ol start="3"><li>one<ul><li>nested</li></ul></li></ol>`. On import, an HTML run in a cell that opens with a block tag goes through the HTML extension's block import, so it comes back as the same blocks instead of being flattened into the paragraph. Without `MdastHtmlExtension`, the export is unchanged.
Paragraphs stay bare lines separated by `<br>`. A block needs no `<br>` beside it, so a `<br>` next to a block stands for an empty paragraph on that side, and empty paragraphs round-trip. Task lists use GitHub's `contains-task-list` and `task-list-item` classes, which the list DOM import rules read back as a check list, and a code block's language is written as `data-language` on the `<pre>`.
## Test plan
### Before
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastTableHtml.test.ts
× writes blocks in a cell as one line of HTML and reads them back
× reads a GitHub task list, a quote and a rule in a cell
× reads x<ul><li>i</li></ul>y as the cell blocks it separates
× reads <br><ul><li>i</li></ul> as the cell blocks it separates
× reads <ul><li>i</li></ul><br> as the cell blocks it separates
× reads <ul><li>i</li></ul><br><br>y as the cell blocks it separates
× reads <ul><li>i</li></ul> <h2>h</h2> as the cell blocks it separates
Tests 7 failed | 1 passed (8)
```
### After
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-mdast packages/lexical-markdown packages/lexical-playground
Tests 1230 passed (1230)
$ ./node_modules/.bin/vitest run --project browser dev-examples/mdast-editor packages/lexical-mdast
Tests 43 passed (43)
```
The new browser test checks that a list and a code block in a cell of the mdast-editor demo round-trip. eslint, prettier, `tsc -p .`, `pnpm run tsc-dev-examples` and the source tree-shaking test are clean. Browser tests ran in Chromium only; Firefox and WebKit aren't installed here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…spans and escapes
## Description
The playground's `TABLE` transformer turned every `<br>` (and legacy `\n`) in a cell's raw Markdown into a line break before parsing it, so a code span `` `<br>` `` came back as two stray backticks around a line break, and an escaped `\<br>` as a lone `\`. Cell decoding now skips code spans and backslash escapes, unescaping only `\|`, which GFM requires everywhere in a row. Export escapes a `<br>` that is text outside code spans as `\<br>`, so typed text `<br>` no longer reads back as a line break.
## Test plan
### Before
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts
× keeps `<br>` in a cell literal
× keeps \<br> in a cell literal
× keeps `\n` in a cell literal
× escapes a literal <br> outside code spans and reads it back
Tests 4 failed | 16 passed (20)
```
### After
```
$ ./node_modules/.bin/vitest run --project unit packages/lexical-mdast packages/lexical-markdown packages/lexical-playground
Tests 1235 passed (1235)
```
eslint, prettier and `tsc -p .` are clean.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Description
The mdast-editor demo cleared its footnotes section whenever the body was empty after an update, so pressing Enter in an already-empty body deleted definitions typed into the Markdown pane. The RootNode transform now clears them only when the body had content before the update, so a deletion still clears them and editing an empty body doesn't.
## Test plan
### Before
```
$ PLAYWRIGHT_BROWSERS_PATH=... ./node_modules/.bin/vitest run --project browser dev-examples/mdast-editor/src/__tests__/browser/MdastEditorChrome.test.ts -t "already-empty"
× typing in an already-empty body keeps definitions
AssertionError: expected '' to be '[^a]: saved note' // Object.is equality
Tests 1 failed | 22 skipped (23)
```
### After
```
$ PLAYWRIGHT_BROWSERS_PATH=... ./node_modules/.bin/vitest run --project browser dev-examples/mdast-editor packages/lexical-mdast
Tests 44 passed (44)
```
`pnpm run tsc-dev-examples` is clean. Browser tests ran in Chromium only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ression ## Description The 12 tests brought in from upstream PR 9333 repeated cases this branch's `CodeSpanEscapes.test.ts` already had (a code span that starts a paragraph, bold code, code after text) or passed with or without the fix. They are replaced by the one case they added that the branch didn't check: the text after a leading code span still reads its escapes (`` `\*` \* &facebook#160; ``). The branch's own escape tests are trimmed to cases that fail without a part of the fix: a code span that starts a paragraph and bold code, which fail without skipping the unescape of code text; bold text, which fails without the `return` after the format transformers; and text before a link, which fails without the `return` after the text match transformers and wasn't covered before. Cases that went through the same path (`a\|b`, `a\\b`, italic, strikethrough, bold italic) or passed either way (code after text, plain text, text after bold) are dropped. ## Test plan ### Before Each kept case was run with one part of the fix reverted: ``` $ vitest run --project unit packages/lexical-markdown/src/__tests__/unit/CodeSpanEscapes.test.ts # importTextTransformers.ts from main × keeps its backslashes in "`\\*`" × keeps its backslashes in "**`\\*`**" × leaves the text after it to read its escapes × reads "**a\\\\\\*b**" as "a\\*b" $ (same, without the return after the format transformers) × reads "**a\\\\\\*b**" as "a\\*b" $ (same, without the return after the text match transformers) × reads "a\\\\\\*b [x](https://y)" as "a\\*b x" ``` ### After ``` $ vitest run --project unit packages/lexical-markdown Tests 641 passed (641) ``` Co-Authored-By: TABISH AYUB RAZA <110284824+Tabisharaza@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ext in a cell, read raw text elements whole, and keep paragraphs in an htmlBlock apart ## Description Fixes from a /code-review of the previous commit on `MdastTableExtension`: - Newlines in raw HTML text were kept only inside `<pre>`, `<textarea>` and `<listing>`, so CSS-preserved whitespace (`white-space: pre-wrap`) and `<xmp>` lost them, the whitespace after a newline was matched with `\s`, which deleted a following no-break space, and trimming the result at every newline was quadratic. Every newline in text or a quoted attribute value is now ` `, which reads back as the same newline; a newline between attributes or in a comment is a space; nothing is trimmed or skipped. - `<textarea>a<b` opened a tag inside the textarea's text. The text of RCDATA elements (`textarea`, `title`) and raw text elements (`script`, `style`, `xmp` and the like) is now read to its end tag; a newline in RCDATA is ` `, and in raw text, which takes no references, a space. - An `htmlBlock` in a cell ran each placeholder child through `flowHtml` alone, so two paragraphs in it ran together. Children with only whitespace between them are now one run, separated by `<br>` as elsewhere. - The cell encoding moves back from a global `html` writer handler, which any other extension's `html` handler would replace and which had no `peek`, to the cell's mdast, now copied only when the cell holds raw HTML. - The `htmlBlock` placeholder format and its splitter move to `htmlBlockParts.ts`, shared by `MdastHtmlExtension` and the cell export. Not changed: a column whose cells mostly have another alignment than its header cell's takes the majority's. GFM gives a column one alignment, so when its cells disagree some cell's alignment is lost either way, and the majority loses the fewest; the docs describe the rule. ## Test plan ### Before ``` $ vitest run --project unit packages/lexical-mdast × writes raw HTML "<!-- don't -->\n<b\nclass='a b'>y</b>" in a cell on one line 13ms × writes raw HTML "<div style=\"white-space: pre-wrap\">a\n b</div>" in a cell on one line 5ms × writes raw HTML "<textarea>a<b\nc</textarea><script>a||b\nc</script>" in a cell on one line 5ms × writes an htmlBlock in a cell on one line 8ms AssertionError: expected '<!-- don\'t --> <b class=\'a b\'>y</b>' to be '<!-- don\'t --> <b class=\'a b\'>…' // Object.is equality AssertionError: expected '<div style="white-space: pre-wrap">a …' to be '<div style="white-space: pre-wrap">a&…' // Object.is equality AssertionError: expected '<textarea>a<b c</textarea><script>a&#…' to be '<textarea>a<b c</textarea><script…' // Object.is equality AssertionError: expected '<details><summary> s\|t </summary> b …' to be '<details><summary> s\|t </sum…' // Object.is equality Tests 4 failed | 241 passed (245) ``` ### After ``` $ vitest run --project unit packages/lexical-mdast Tests 245 passed (245) $ vitest run --project browser dev-examples/mdast-editor packages/lexical-mdast Tests 44 passed (44) $ vitest run --project scripts-unit scripts/__tests__/unit/treeShakingSource.test.ts Tests 341 passed (341) $ pnpm run ci-check # exit 0 ``` Browser tests ran in Chromium only; Firefox and WebKit were not exercised. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e/mdast-tables-9323-twjf6u
…n place
## Description
A text-match transformer whose `replace` returns nothing and edits the matched node in place (for example, setting a format on it) left that text's markdown escapes unread, so `@a\*b` imported as `@a\*b` instead of `@a*b`. The import now unescapes the matched node when the transformer returned no node and the matched node is still attached and can contain transformable markdown. The comment on restoring a cancelled line in `MarkdownImport` now says which lines are restored.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-markdown/src/__tests__/unit/CodeSpanEscapes.test.ts
× reads its escapes when the transformer edits it in place: "@A\\*b"
× reads its escapes when the transformer edits it in place: "x @A\\*b"
Tests 2 failed | 5 passed (7)
```
### After
```
$ vitest run --project unit packages/lexical-markdown/src/__tests__/unit/CodeSpanEscapes.test.ts
Tests 7 passed (7)
```
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e or block syntax
## Description
The playground's markdown table transformer lost or changed cell content in a few cases:
- A body row whose cells are only dashes and colons (`-`, `:-:`) was written as-is, so it read back as a delimiter row. The first `-` or `:` of such a row is now escaped.
- A line after a line break or paragraph break that starts like a block (`---`, `# h`, `> q`, `- i`, `1. i`) was read back as that block. Each line after the first now has its block start escaped.
- A paragraph holding only whitespace was dropped. Its whitespace is now written as character references.
- Importing a table moved the selection to the table's end even when the table was generated without a selection. The transformer now selects the end only when the target is attached, and cells are built with `$generateNodesFromMarkdownString` so nothing touches the selection.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts
× keeps a link whose second line starts like a list item
× keeps a row whose cells are only "-"
× keeps a row whose cells are only ":-:"
× keeps a row whose cells are only "---"
× keeps a row whose cells are only "-:"
× keeps a line "---" in a cell as text
× keeps a line "# h" in a cell as text
× keeps a line "> q" in a cell as text
× keeps a line "- i" in a cell as text
× keeps a line "1. i" in a cell as text
× keeps a paragraph that is only whitespace
× leaves the selection alone when it generates a table
Tests 12 failed | 126 passed (138)
```
### After
```
$ vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts
Tests 138 passed (138)
```
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the header's alignment
## Description
- GFM trims whitespace around a cell and around each `<br>`, so spaces at the start or end of a cell line, or a paragraph of only spaces, were lost on a round trip. Export now writes those spaces as character references, and import only trims whitespace that was written literally in the source, so a hand-written `a <br> b` still reads as `a` and `b`.
- Exporting a selection that left out the header row ignored the header cell's alignment as the fallback for a column without a majority. The header row's alignment is now read whether or not it is in the selection.
- The mdast-editor demo's footnote extension reads with `editor.read('latest', ...)` instead of `getEditorState().read`.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastTableHtml.test.ts packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts
× keeps the spaces at the edges of a line in a cell (HTML extension: false)
× keeps the spaces at the edges of a line in a cell (HTML extension: true)
× takes the header cell's alignment into a selection without it
Tests 3 failed | 135 passed (138)
```
### After
```
$ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastTableHtml.test.ts packages/lexical-mdast/src/__tests__/unit/MdastImportExport.test.ts
Tests 138 passed (138)
$ vitest run --project unit packages/lexical-markdown packages/lexical-playground/__tests__/unit packages/lexical-code-core packages/lexical-react packages/lexical-mdast
Tests 1607 passed (1607)
$ vitest run --project browser packages/lexical-mdast dev-examples/mdast-editor
Tests 44 passed (44)
$ pnpm run tsc && pnpm run flow
Found 0 errors
```
Only Chromium was available, so the browser tests did not run in Firefox or WebKit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e/mdast-tables-9323-twjf6u
…character references and code spans of spaces
## Description
- A text-match transformer that edits the matched text in place now has that text's escapes read in nodes generated outside the root too (`$generateNodesFromMarkdownString`), where nothing is attached. The check is now that the node still has a parent.
- `unescapeText` threw a `RangeError` on a character reference to no valid character, such as `�`. As in CommonMark, `�`, a surrogate or a code point past U+10FFFF now reads as U+FFFD.
- A code span of only spaces was written with a padding space on each side, which a reader keeps, so it grew on each round trip (`` ` ` `` became three spaces). It is now written bare.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-markdown/src/__tests__/unit/CodeSpanEscapes.test.ts
× round-trips " ", which is only spaces
× round-trips " ", which is only spaces
× reads its escapes in nodes generated outside the root
× reads "�", which names no valid character, as U+FFFD
× reads "�", which names no valid character, as U+FFFD
× reads "�", which names no valid character, as U+FFFD
```
### After
```
$ vitest run --project unit packages/lexical-markdown packages/lexical-mdast packages/lexical-playground/__tests__/unit
Tests 1295 passed (1295)
```
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sformer and trim only spaces and tabs
## Description
- A cell line that starts and ends with a pipe (`|q|`) read back as a table nested in the cell, losing its pipes. A GFM cell can't hold a table, so cells are now read with every playground transformer except `TABLE`.
- Cell padding was trimmed with `String.prototype.trim`, which also removed spaces such as U+3000 and U+00A0. GFM trims only spaces and tabs, and so does the import now.
- A paragraph of only whitespace was written as character references from its text, which dropped formatting such as a code span. Only a paragraph whose Markdown is whitespace is written that way now.
- The export retry loop could run forever if a mark turned up more often than the nodes written as it for a reason other than a collision. A retry now avoids all of the first try's Markdown, so a second mismatch can't be a collision and is taken as written.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts
× keeps a line that starts and ends with a pipe as text
× keeps a code span that is only whitespace
× trims only spaces and tabs from a cell
```
### After
```
$ vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts
Tests 142 passed (142)
```
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l and guard missing offsets ## Description - A line of only spaces right after a block in a cell (`<ul><li>a</li></ul>  <br>`) was dropped on import, since any whitespace there was taken as padding. Only whitespace written literally is dropped now. - Telling literal spaces from references sliced the source by a node's offsets without checking that they exist, so a tree whose positions have only lines and columns sliced the whole document and stopped trimming `a <br> b`. Without offsets, all edge spaces are taken as literal, as without a source. - Each line before a block in a cell is trimmed once instead of twice. ## Test plan ### Before ``` $ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastTableHtml.test.ts × keeps a line of only spaces after a block in a cell × trims the spaces around a <br> in a tree without offsets ``` ### After ``` $ vitest run --project unit packages/lexical-markdown packages/lexical-playground/__tests__/unit packages/lexical-code-core packages/lexical-react packages/lexical-mdast Tests 1628 passed (1628) $ vitest run --project browser packages/lexical-mdast dev-examples/mdast-editor Tests 44 passed (44) $ pnpm run tsc && pnpm run flow Found 0 errors ``` Only Chromium was available, so the browser tests did not run in Firefox or WebKit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd columns ## Description Telling the spaces a cell line was written with from those written as character references needs a node's offsets in the source. A tree whose positions have only lines and columns, as a tree transform may leave them, took every edge space as literal, so `a<br> b` read back as `a` and `b`. Offsets are now worked out from the line and column when a point has none. The three places that ended a line in `cellItems` are one helper again, and its doc comment states which whitespace after a block makes a line: anything but spaces and tabs written as they are, so a no-break space after a block, which GFM doesn't trim, is kept as a line too. ## Test plan ### Before ``` $ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastTableHtml.test.ts × reads "a<br> b" in a tree without offsets as ["a"," b"] Tests 1 failed | 23 passed (24) ``` ### After ``` $ vitest run --project unit packages/lexical-mdast Tests 252 passed (252) $ vitest run --project browser packages/lexical-mdast dev-examples/mdast-editor Tests 44 passed (44) ``` Only Chromium was available, so the browser tests did not run in Firefox or WebKit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oes and pad code spans only for spaces ## Description - `unescapeText` read backslash escapes and character references in two passes, so an escaped `\A` came back as `A` instead of the text `A`. It now reads both in one pass. Export writes `\&` before text that would read as a reference, so text like ` ` round-trips. - A decimal reference with more than 7 digits (`�`) is not a reference in CommonMark and now stays text instead of becoming U+FFFD. - A code span was padded when it started and ended with any whitespace, but a reader strips only U+0020, so spans like a tab-wrapped `x` or a lone no-break space are now written without padding. ## Test plan ### Before ``` $ vitest run --project unit packages/lexical-markdown/src/__tests__/unit/CodeSpanEscapes.test.ts × round-trips "\tx\t" bare × round-trips " " bare × reads "�" as the text "�" × reads "\\A" as the text "A" × round-trips text that reads as one ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯ Tests 5 failed | 13 passed (18) ``` The second line is the no-break space case, which the terminal shows as a space. ### After ``` $ vitest run --project unit packages/lexical-markdown/src/__tests__/unit/CodeSpanEscapes.test.ts Tests 18 passed (18) ``` Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hout dropping the table transformer ## Description - A cell paragraph whose content writes nothing (an empty text node before normalization) was no longer written as an empty line. It is again. - Reading a cell without `TABLE` made export and import disagree and filtered the transformers twice per cell. Cells are read with all of `PLAYGROUND_TRANSFORMERS` again, and a cell line that reads as a table row (`|q|`) is written starting with `&facebook#124;` so it stays text. - The export retry took mismatched marks as written after one try, which could add stray `<br>`s. Marks are now counted per write rather than per node, so a node written twice counts twice, and a retry happens only on a real collision, which the next try avoids. ## Test plan ### Before ``` $ vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts × writes a paragraph whose content writes nothing as an empty line ``` ### After ``` $ vitest run --project unit packages/lexical-playground/__tests__/unit/MarkdownTransformers.test.ts Tests 142 passed (142) ``` With the `&facebook#124;` escape disabled, "keeps a line that starts and ends with a pipe as text" fails, so that case stays covered now that `TABLE` reads cells again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Description
A tree transform can leave points with only a line and column. The table extension worked out offsets itself with a module-level cache that kept the last source alive and miscounted a bare newline followed by another, while every other handler that reads the source by offset (heading style, list markers, code fences) fell back to its no-source behavior. The import (and the streaming parser) now fill in each missing offset once, after parsing, from the line and column, so every reader gets it and nothing is cached across imports.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastTableHtml.test.ts
× reads "h\n=\n\n\n""a<br> b" in a tree without offsets
AssertionError: expected '# h\n\n| a |\n| -----------…' to match /^h\n=+\n/
```
### After
```
$ vitest run --project unit packages/lexical-markdown packages/lexical-playground/__tests__/unit packages/lexical-code-core packages/lexical-react packages/lexical-mdast
Tests 1636 passed (1636)
$ vitest run --project browser packages/lexical-mdast dev-examples/mdast-editor
Tests 44 passed (44)
$ vitest run --project scripts-unit scripts/__tests__/unit/treeShakingSource.test.ts
Tests 342 passed (342)
$ pnpm run tsc && pnpm run flow
Found 0 errors
```
Only Chromium was available, so the browser tests did not run in Firefox or WebKit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nothing can drop offsets
## Description
- Filling missing offsets assigned to each point in place, so a transform that shares one frozen position made the import throw. Frozen points are now left without an offset, which the handlers already fall back on.
- The full import and the streaming parser now parse through one `parseMarkdown` helper, so no entry point can parse without filling offsets. It walks the tree only when a registered mdast extension has tree transforms, since only a transform can leave a point without an offset. Where it does walk, the cost is small: on a 443 KB document, parsing took 4.2 s per run and the walk 31 ms.
- Tests now cover the source-reading paths on a tree whose offsets were dropped: setext headings, list markers, code fences, line breaks of spaces, autolinks, a reference with no definition, and the block and inline shortcuts while typing. The table test no longer asserts on a heading behind a condition.
## Test plan
### Before
```
$ vitest run --project unit packages/lexical-mdast/src/__tests__/unit/MdastSourceOffsets.test.ts
× imports with a frozen position shared by its nodes
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
TypeError: Cannot add property offset, object is not extensible
Tests 1 failed | 8 passed (9)
```
The other cases are new coverage for 08ffa4c. Against the sources before it, five of them fail (setext heading, list marker, code fence, line break of spaces, inline shortcut).
### After
```
$ vitest run --project unit packages/lexical-markdown packages/lexical-playground/__tests__/unit packages/lexical-code-core packages/lexical-react packages/lexical-mdast
Tests 1645 passed (1645)
$ vitest run --project browser packages/lexical-mdast dev-examples/mdast-editor
Tests 44 passed (44)
$ vitest run --project scripts-unit scripts/__tests__/unit/treeShakingSource.test.ts
Tests 342 passed (342)
$ pnpm run tsc && pnpm run flow
Found 0 errors
```
Only Chromium was available, so the browser tests did not run in Firefox or WebKit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch was successfully deployed
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.
Description
A GFM table cell holds a single line, so line breaks in a cell need the
<br>convention that GitHub and most renderers accept there.TABLEMarkdown transformer wrote a literal\nfor each newline in a cell (one\n\ntwo), which no renderer reads. It now writes<br>, escapes|in cell text, reads<br>,<br/>and<br />back (the legacy\ntoo) except inside code spans and after a backslash escape, escapes a<br>typed as text, writes each line of a fenced code block with its own<br>(so blank lines survive) and backslash-escapes any\,|or<br>in the code so it reads back verbatim, keeps bold, italic, strikethrough and links whole when a<br>falls inside them, gives each pipe in inline code a backslash of its own and writes code with a backslash right before a pipe, which a GFM cell can't hold as a code span, as<code>with character references, splits rows only on unescaped pipes (a pipe after an odd run of backslashes is escaped, as in micromark:\|and\\\|are content,\\|splits), trims cell padding on import, writes the space or tab next to a line break as a character reference so it isn't trimmed as padding, writes an empty paragraph as an empty line (a<br><br>b), reads a row of empty cells (| | |) as a row rather than a delimiter row, and reads a cell with many line breaks in linear time. A hard line break's\marker no longer lands in front of the<br>(a\<br>bread back as an escaped<), a single-column table with a header row now gets its delimiter row, and| --- |with no table above it stays text instead of becoming an empty paragraph.@lexical/markdownread the backslash escapes in formatted text twice, so**a\\\*b**came back asa*b; each escape is now read once.@lexical/markdowndropped backslashes from a code span that was all of a text node's text (`\*`read back as*,` `as a no-break space), though code spans take no escapes; it now leaves code-formatted text alone. This is the fix in [lexical-markdown] Bug Fix: Preserve literal inline-code content during import #9333; the one case its tests check that this PR's didn't (text after a leading code span still reads its escapes) is included here.@lexical/markdownkept a line with its matched prefix cut off when an element transformer cancelled its import by returningfalse. It now restores the whole line, unless the transformer changed or moved the text before cancelling, in which case its changes stay as before.@lexical/mdast'sMdastTableExtensionexported a cell's paragraphs and line breaks as spaces, so the lines were lost, and fused the items of a list in a cell (xy). It now writes each paragraph and line break as its own<br>-separated line, and imports<br>as a paragraph boundary (nested in formatting, as a line break). Without this,<br>imported as literal text and exported as\<br>.MdastHtmlExtension(a peer dependency ofMdastTableExtension), lists, task lists, headings, quotes, code blocks and rules in a cell are written as one line of HTML around their Markdown content, such asintro<ol start="3"><li>one<ul><li>nested</li></ul></li></ol>, which GFM allows in a cell, and read back as the same blocks. Without it, they are flattened into<br>-separated lines. Raw HTML in a cell is written on one line: a newline in its text or a quoted attribute value becomes , so whitespace that<pre>or CSS keeps survives, a newline between attributes or in a comment becomes a space, the text of<textarea>,<script>and the like is read to its end tag, and every pipe becomes|, so<span title="a|b">doesn't split the row. AnhtmlBlock(rawHtmlBlock) in a cell is written as one line of its HTML around its Markdown children.@lexical/mdastwas stored as one array on the table, so deleting or inserting a column shifted every alignment after it, and the editor never showed it. It is now the element format of each cell in the column, which renders astext-alignand whichFORMAT_ELEMENT_COMMANDalready sets on a cell selection; export also reads the format a cell's paragraphs share, and since GFM can't align one cell, a column takes the alignment a strict majority of its cells have, or else that of its cell in the table's header row, so inserted rows don't clear it. An existing document's old table-level state is moved onto its cells that have no format of their own when the table loads and then cleared, so it can't override a column whose alignment is later cleared or moved. A cell that already has a format keeps it, includingjustify,startandend, which GFM can't write; the docs say so and tests cover it.TableExtension(with the features GFM can't express turned off), Insert > Table, a Table menu for adding and removing rows and columns and aligning columns, table styles, and a Tables section in the demo document.theme.blockCursor) Lexical draws between adjacent tables or decorators.Closes #9323
Test plan
Before
After
The new browser test
dev-examples/mdast-editor/src/__tests__/browser/TableEdit.test.tscovers the Table menu's commands, pressing Enter in a cell, which exports| 1 | 2<br>3 |, and a list and code block in a cell round-tripping as HTML.MdastTableHtml.test.tscovers the HTML cell format, and the playground and markdown tests cover the issue comments' cases. Browser tests ran in Chromium only; Firefox and WebKit were not exercised.