diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index edea7de8d..cf21f7f40 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -302,6 +302,27 @@ Average Latency: 1,240 ms expect(plainLines.filter(line => line.includes("+"))).toHaveLength(0); }); + it("keeps a four-space-indented tree child inside its paragraph", () => { + const markdown = new Markdown( + `Two verified cases: + +├── **case 1** — first branch +│ └── each family has its own counter +└── **case 3: unnumbered limits** + └── limits that carry **only a label** are dropped from the list`, + 0, + 0, + defaultMarkdownTheme, + ); + const plainLines = markdown.render(80).map(line => stripVTControlCharacters(line).trimEnd()); + + // The attached indented line is a lazy paragraph continuation, never an + // indented code block: no fence border rows, no literal bold markers. + expect(plainLines.filter(line => line.includes("```"))).toHaveLength(0); + expect(plainLines.some(line => line.includes("only a label"))).toBe(true); + expect(plainLines.some(line => line.includes("**"))).toBe(false); + }); + it("keeps an unfinished code block with a rule and heading literal when no table follows", () => { const markdown = new Markdown( `The process printed this diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 3bab19b75..80c4a0d29 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the vendored Markdown lexer letting an indented code block interrupt a paragraph: a 4-space-indented line directly attached to paragraph text (e.g. a box-drawing tree child under a `└──` branch) now stays a lazy continuation of that paragraph, matching marked/CommonMark, instead of rendering the rest of the message section as a raw code block ([#8557](https://github.com/can1357/oh-my-pi/pull/8557) by [@chan1103](https://github.com/chan1103)). + ## [17.3.2] - 2026-08-13 ### Fixed diff --git a/packages/utils/src/marked/core.ts b/packages/utils/src/marked/core.ts index c9f30bc6d..e73b36920 100644 --- a/packages/utils/src/marked/core.ts +++ b/packages/utils/src/marked/core.ts @@ -1071,8 +1071,20 @@ function blockTokens(src: string, lexer: Lexer, output: Token[]): Token[] { } let raw = line; i++; - while (i < lines.length && !isBlockStart(lines, i)) { - raw += lines[i++]!; + // An indented code block cannot interrupt a paragraph (CommonMark lazy + // continuation): a 4-space-indented line directly attached to nonblank + // paragraph text stays inside the paragraph, bypassing every block-start + // probe — matching marked's paragraph rule. After a whitespace-padded + // blank line the indent is no longer attached, so it still opens an + // indented code block. + let prevBlankish = false; + while (i < lines.length) { + const next = lines[i]!; + const lazyIndent = !prevBlankish && /^ {4}\S/.test(next); + if (!lazyIndent && isBlockStart(lines, i)) break; + prevBlankish = next.trim() === ""; + raw += next; + i++; } const text = stripFinalNewline(raw); const tokens: Token[] = []; diff --git a/packages/utils/test/fixtures/marked/goldens.json b/packages/utils/test/fixtures/marked/goldens.json index 1b8a00536..68e7e7fd7 100644 --- a/packages/utils/test/fixtures/marked/goldens.json +++ b/packages/utils/test/fixtures/marked/goldens.json @@ -954,5 +954,35 @@ } ], "html": "
inline x end
\n" + }, + { + "name": "lazyContinuation", + "source": "tree root\n└── last branch\n └── child under last branch\n\n real indented code\n", + "tokens": [ + { + "type": "paragraph", + "raw": "tree root\n└── last branch\n └── child under last branch", + "text": "tree root\n└── last branch\n └── child under last branch", + "tokens": [ + { + "type": "text", + "raw": "tree root\n└── last branch\n └── child under last branch", + "text": "tree root\n└── last branch\n └── child under last branch", + "escaped": false + } + ] + }, + { + "type": "space", + "raw": "\n\n" + }, + { + "type": "code", + "raw": " real indented code\n", + "codeBlockStyle": "indented", + "text": "real indented code\n" + } + ], + "html": "tree root\n└── last branch\n └── child under last branch
\nreal indented code\n\n"
}
]
diff --git a/packages/utils/test/marked.test.ts b/packages/utils/test/marked.test.ts
index c53ac7408..1448b8044 100644
--- a/packages/utils/test/marked.test.ts
+++ b/packages/utils/test/marked.test.ts
@@ -81,6 +81,37 @@ describe("marked compatibility", () => {
});
}
+ // Lazy-continuation boundary shapes (behavior cross-checked against marked
+ // v18): an indented code block cannot interrupt a paragraph, so an indented
+ // line directly attached to paragraph text stays in the paragraph even when
+ // a setext/hr lookahead matches downstream — while a whitespace-padded
+ // blank line detaches it, so the next indented run still opens indented
+ // code. Documented divergences from marked kept as-is: marked dedents the
+ // attached line inside `text` via its code-merge path, and it splits a
+ // padded blank into a `space` token where this lexer keeps the padded
+ // blank inside the paragraph raw.
+ const lazyBoundaryShapes: Array<[string, Array<[string, string]>]> = [
+ [
+ "lead\n \n code\n",
+ [
+ ["paragraph", "lead\n \n"],
+ ["code", " code\n"],
+ ],
+ ],
+ [
+ "lead\n attached\n---\n",
+ [
+ ["paragraph", "lead\n attached\n"],
+ ["hr", "---\n"],
+ ],
+ ],
+ ];
+ for (const [source, shape] of lazyBoundaryShapes) {
+ test(`keeps the lazy-continuation boundary shape for ${JSON.stringify(source)}`, () => {
+ expect([...Lexer.lex(source)].map(token => [token.type, token.raw])).toEqual(shape);
+ });
+ }
+
test("runs block and inline tokenizer/renderer extensions", () => {
const latexBlock: TokenizerAndRendererExtension = {
name: "latexBlock",