Repository navigation
Bug: A line starting with three backticks and with more backticks later is imported as a code block, including Lexical's own export of inline code #9404
Description
Lexical version: 0.52.0 (main at 98e936d gives the same results)
Steps to reproduce
Make a paragraph whose first child is inline code containing two or more backticks in a row, optionally followed by text. Export it with $convertToMarkdownString(TRANSFORMERS), then import the result with $convertFromMarkdownString(markdown, TRANSFORMERS):
inline code a``b + text " x" → exported ``` a``b ``` x → imported as code block a``b ``` x
inline code ``` + text " x" → exported ```` ``` ```` x → imported as code block ``` ```` x (and every line after it)
inline code a``b → exported ``` a``b ``` → imported as code block " a``b "
inline code `a` + text " x" → exported `` `a` `` x → imported as a paragraph ✅
Importing Markdown written elsewhere behaves the same:
```` ``` ```` x
nextis imported as one code block holding ``` ```` x, a blank line and next. CommonMark reads it as a paragraph with the code span ``` and the text x, followed by a paragraph next.
Current behaviour
A line that starts with three or more backticks is taken as an opening fence, even when backticks follow later on the line. CommonMark says the info string of a backtick fence may not contain backticks, so such a line isn't a fence. It's a paragraph, usually starting with a code span. When no closing fence follows, the code block runs to the end of the document.
exportTextFormat() writes inline code with a fence longer than any backtick run inside it, so inline code that holds `` at the start of a paragraph always produces such a line. The editor's own output therefore turns into a code block on the next load.
Cause
CODE_START_REGEX in packages/lexical-markdown/src/MarkdownTransformers.ts is /^([ \t]*{3,})([\w-]+)?[ \t]?/, which accepts anything after the fence. CODE.handleImportAfterStartMatch()then either takes the line as the single-line form (closing backticks at the end of the line) or opens a multi-line block.normalizeMarkdown()` uses the same expression, so it also stops merging the lines that follow.
The single-line form, ```Single line Code``` as a code block, is covered by a test ('```Single line Code```' in LexicalMarkdown.test.ts), so the fix below keeps it when its code has no backticks.
Suggested fix
--- a/packages/lexical-markdown/src/MarkdownTransformers.ts
+++ b/packages/lexical-markdown/src/MarkdownTransformers.ts
@@ -222,7 +222,11 @@ const UNORDERED_LIST_REGEX = /^(\s*)[-*+]\s/;
const CHECK_LIST_REGEX = /^(\s*)(?:[-*+]\s)?\s?(\[(\s|x)?\])\s/i;
const HEADING_REGEX = /^(#{1,6})\s/;
const QUOTE_REGEX = /^>\s/;
-const CODE_START_REGEX = /^([ \t]*`{3,})([\w-]+)?[ \t]?/;
+// The info string of a backtick fence cannot contain a backtick, so a line
+// with one after the fence is not a fence, except the single-line form
+// ```code``` whose code has none.
+// https://spec.commonmark.org/0.31.2/#fenced-code-blocks
+const CODE_START_REGEX = /^([ \t]*`{3,})(?=[^`]*$|[^`]+`{3,}$)([\w-]+)?[ \t]?/;
const CODE_END_REGEX = /^[ \t]*`{3,}$/;
const CODE_SINGLE_LINE_REGEX =
/^[ \t]*```[^`]+(?:(?:`{1,2}|`{4,})[^`]+)*```(?:[^`]|$)/;
@@ -912,6 +916,12 @@ export const CODE: MultilineElementTransformer = {
return [true, startLineIndex];
}
+ // A closing run shorter than the opening one, as in ````code```, leaves a
+ // backtick in the info string, so the line is not a fence.
+ if (afterFence.includes('`')) {
+ return null;
+ }
+
const multilineEndRegex = new RegExp(`^[ \\t]*\`{${fenceLength},}$`);With this change, all the rows above round-trip unchanged as paragraphs with inline code, and next stays its own paragraph. These still import as before: ```Single line Code``` and ```a``` (single-line code blocks), ```js with code and a closing fence, a fence with title="x" metadata, nested / ```` fences, and the unclosed ````javascript Incomplete tag . `` a``` `` is now a paragraph instead of a code block with the language a.
Tested with headless Lexical against main at 98e936d. The unit tests for lexical-markdown, lexical-mdast and lexical-playground/__tests__/unit pass (1175), along with tsc, ESLint and Prettier. No new unit tests are added, and the typing shortcut wasn't tested in a browser. It uses the same expression, and a fence typed as ``` plus a space still matches.
Notes:
- The single-line form isn't CommonMark:
```a```is a paragraph with the code spanathere. Dropping the form would make Lexical's import fully match CommonMark here, but it would break the existing test, so the diff keeps it. @lexical/mdastreads all these lines as CommonMark does.
Related: #8109 and #8116 (nested fences), #8734 (fence-like lines inside a code block), #9055 (info string metadata).
Activity