Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ragraph line on export ## Description Markdown export wrote the text of a root paragraph as it was, so a paragraph whose text starts like a block (`# Hours`, `> a`, `- a`, `+ a`, `1. a`) was read back by the next import as a heading, a quote or a list. The text escape in `exportTextFormat` only covers the inline characters `*_`~\`, and nothing in the export knew about the start of a line. A paragraph that no element transformer handles now has the first character of each line escaped when the line starts with a marker that the import recognizes: `#`s or `>` followed by whitespace, and an indented `-`, `+` or `N.` followed by whitespace, the way `$listExport` already escapes `1.`. The escape runs on the exported text of the paragraph, not on each TextNode, so a marker split across formatted nodes (`#` followed by a bold ` h`) is covered as well. Content of a quote, a heading or a list item does not go through it. Text that is not a marker keeps its export: `#hashtag`, `-1`, `a - b`, `1) x`, `---`, `> - a`, and `- a` followed by a blank line and ` > q`. Closes meta#9401 ## Test plan ### Before ``` $ pnpm exec vitest --project unit --no-watch packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts ❯ |unit| packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts (615 tests | 5 failed) ❯ a paragraph line that starts like a block (5) × reads "\\# Hours" back as a paragraph × reads "\\> a" back as a paragraph × reads "\\- a" back as a paragraph × reads "\\+ a" back as a paragraph × reads "1\\. a" back as a paragraph AssertionError: expected [ [ 'heading', 'Hours' ] ] to deeply equal [ [ 'paragraph', '# Hours' ] ] AssertionError: expected [ [ 'quote', 'a' ] ] to deeply equal [ [ 'paragraph', '> a' ] ] AssertionError: expected [ [ 'list', 'a' ] ] to deeply equal [ [ 'paragraph', '- a' ] ] AssertionError: expected [ [ 'list', 'a' ] ] to deeply equal [ [ 'paragraph', '+ a' ] ] AssertionError: expected [ [ 'list', 'a' ] ] to deeply equal [ [ 'paragraph', '1. a' ] ] Test Files 1 failed (1) Tests 5 failed | 610 passed (615) ``` ### After ``` $ pnpm exec vitest --project unit --no-watch packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts Test Files 1 passed (1) Tests 615 passed (615) $ pnpm exec vitest --project unit --no-watch packages/lexical-markdown packages/lexical-mdast Test Files 15 passed (15) Tests 917 passed (917) ``` `prettier --check` and `eslint` are clean on the two changed files, and `tsc` is clean on a project holding `lexical-markdown` and `lexical-mdast` with their tests. Not exercised: Node 22.23.2 only (`.nvmrc` asks for 24.21.0), jsdom unit tests only (no browser or e2e run), no Flow check, and `tsc` on that slice rather than the whole monorepo.
…ner marker precedes a bracket
67ad756 to
c9c762d
Compare
|
Rebased onto main and resolved the import conflict in MarkdownExport.ts. With this fix, five of the "container marker precedes it" cases from #9600 failed, so I updated their expectations in a separate commit. Their old expected output (for example |
lexical
@lexical/a11y
@lexical/clipboard
@lexical/code
@lexical/code-core
@lexical/code-prism
@lexical/code-shiki
@lexical/compiler
@lexical/devtools-core
@lexical/dragon
@lexical/eslint-plugin
@lexical/extension
@lexical/fast-check
@lexical/file
@lexical/hashtag
@lexical/headless
@lexical/history
@lexical/html
@lexical/internal
@lexical/link
@lexical/list
@lexical/mark
@lexical/markdown
@lexical/mdast
@lexical/offset
@lexical/overflow
@lexical/plain-text
@lexical/react
@lexical/rich-text
@lexical/selection
@lexical/table
@lexical/tailwind
@lexical/text
@lexical/utils
@lexical/y
@lexical/yjs
commit: |
There was a problem hiding this comment.
Thanks for the fix, this looks good. Paragraphs whose lines start with # , > , - , + or N. now export with the marker escaped and import back as paragraphs, and the new cases fail without the change and pass with it. Everything below is optional and non-blocking.
Optional suggestions
-
Escape per
\nline instead of with themflag (packages/lexical-markdown/src/MarkdownExport.ts:414-419). Withm,^also matches after\r, U+2028 and U+2029, while the import splits lines on\nonly (MarkdownImport.ts:56). Because the escape runs over the finished Markdown, a paragraph holding inline codex\r- ynow exports as`x\r\- y`and imports back with the backslash inside the code text (CommonMark: backslash escapes do not work in code spans). Escaping each line ofmarkdown.split('\n')avoids that, and lets the indent be\s*likeUNORDERED_LIST_REGEXandORDERED_LIST_REGEX(MarkdownTransformers.ts:226-227), so\u00A0- aor\u30001. a, which still import as list items, are covered too;MarkdownEscaping.test.tsalready covers that Unicode indentation for task markers. For example:function escapeBlockMarkers(markdown: string): string { return markdown .split('\n') .map(line => line .replace(/^(#{1,6}|>)(?=\s)/, '\\$1') .replace(/^(\s*)([-+])(?=\s)/, '$1\\$2') .replace(/^(\s*\d+)\.(?=\s)/, '$1\\.'), ) .join('\n'); }
-
Apply the same escape in
$convertSelectionToMarkdownString. The selection exporter's paragraph path (MarkdownExport.ts:212-223) still returns# Hoursfor a selected# Hoursparagraph, which imports as a heading, while$convertToMarkdownStringnow returns\# Hours. -
A few more test rows for what the description relies on: a marker after a line break inside a paragraph, an indented marker (
- a), the#plus boldhsplit case, and text that must stay as typed (#hashtag,-1,a - b,1) x,---, a quote holding- a). -
Changeset. CONTRIBUTING.md ("Changesets") asks for one when a published package's output changes, with a
Verify:paragraph for a fix like this one, for example.changeset/markdown-escape-block-markers.md:--- '@lexical/markdown': patch --- [fix] Markdown export escapes `#`, `>`, `-`, `+` and `1.` at the start of a paragraph line, so text such as `# Hours` or `- a` imports again as a paragraph instead of a heading, quote or list Verify: Markdown export snapshots of paragraphs whose lines start with `# `, `> `, `- `, `+ ` or a number and `. `, which now carry a backslash before the marker (`\# Hours`, `\- a`, `1\. a`), and that whatever renders or stores the exported Markdown reads those escapes as the literal character.
A maintainer can also add it for you.
| .replace(/^(#{1,6}|>)(?=\s)/gm, '\\$1') | ||
| .replace(/^([ \t]*)([-+])(?=\s)/gm, '$1\\$2') | ||
| .replace(/^([ \t]*\d+)\.(?=\s)/gm, '$1\\.'); |
There was a problem hiding this comment.
Optional: with the m flag, ^ also matches after \r, U+2028 and U+2029, but the import splits lines on \n only, so a paragraph holding inline code x\r- y now exports as `x\r\- y` and keeps the backslash on import (backslash escapes do not work in code spans). Escaping each line of markdown.split('\n') without m avoids that, and lets the indent be \s* like UNORDERED_LIST_REGEX/ORDERED_LIST_REGEX, which also covers \u00A0- a and \u30001. a.
| undefined, | ||
| shouldPreserveNewLines, | ||
| ); | ||
| return $isParagraphNode(node) ? escapeBlockMarkers(markdown) : markdown; |
There was a problem hiding this comment.
Optional: the selection exporter's paragraph path ($processNodeForSelection, lines 212-223) doesn't get this escape, so $convertSelectionToMarkdownString still returns # Hours for a selected # Hours paragraph, which imports as a heading.
Description
Markdown export wrote the text of a root paragraph as it was, so a paragraph whose text starts like a block (
# Hours,> a,- a,+ a,1. a) was read back by the next import as a heading, a quote or a list. The text escape inexportTextFormatonly covers the inline characters*_~`, and nothing in the export knew about the start of a line.A paragraph that no element transformer handles now has the first character of each line escaped when the line starts with a marker that the import recognizes:
#s or>followed by whitespace, and an indented-,+orN.followed by whitespace, the way$listExportalready escapes1.. The escape runs on the exported text of the paragraph, not on each TextNode, so a marker split across formatted nodes (#followed by a boldh) is covered as well. Content of a quote, a heading or a list item does not go through it.Text that is not a marker keeps its export:
#hashtag,-1,a - b,1) x,---,> - a, and- afollowed by a blank line and> q.I wrote this with help from Sonnet 5.5 (xhigh) via T3 Code, and reviewed the change and the tests myself.
Closes #9401
Test plan
Before
After
prettier --checkandeslintare clean on the two changed files, andtscis clean on a project holdinglexical-markdownandlexical-mdastwith their tests.Not exercised: Node 22.23.2 only (
.nvmrcasks for 24.21.0), jsdom unit tests only (no browser or e2e run), no Flow check, andtscon that slice rather than the whole monorepo.