From 3f7565abcd70d029e5a401897aacb6adadb8d9bb Mon Sep 17 00:00:00 2001 From: Tuyen Date: Thu, 12 Mar 2026 00:57:02 +0700 Subject: [PATCH] fix(zalouser): preserve literal quote lines in code fences --- extensions/zalouser/src/text-styles.test.ts | 14 ++++ extensions/zalouser/src/text-styles.ts | 72 ++++++++++++++------- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/extensions/zalouser/src/text-styles.test.ts b/extensions/zalouser/src/text-styles.test.ts index 987667d1193..28fb23be058 100644 --- a/extensions/zalouser/src/text-styles.test.ts +++ b/extensions/zalouser/src/text-styles.test.ts @@ -76,6 +76,20 @@ describe("parseZalouserTextStyles", () => { }); }); + it("preserves quote-prefixed lines inside normal fenced code blocks", () => { + expect(parseZalouserTextStyles("```\n> prompt\n```")).toEqual({ + text: "> prompt", + styles: [], + }); + }); + + it("does not treat quote-prefixed fence text inside code as a closing fence", () => { + expect(parseZalouserTextStyles("```\n> ```\n*still code*\n```")).toEqual({ + text: "> ```\n*still code*", + styles: [], + }); + }); + it("keeps unmatched fences literal", () => { expect(parseZalouserTextStyles("```python")).toEqual({ text: "```python", diff --git a/extensions/zalouser/src/text-styles.ts b/extensions/zalouser/src/text-styles.ts index 093c9e2f23b..3d6fbda8807 100644 --- a/extensions/zalouser/src/text-styles.ts +++ b/extensions/zalouser/src/text-styles.ts @@ -34,6 +34,10 @@ type FenceMarker = { indent: number; }; +type ActiveFence = FenceMarker & { + quoteIndent: number; +}; + const TAG_STYLE_MAP: Record = { red: TextStyle.Red, orange: TextStyle.Orange, @@ -102,41 +106,40 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: const lines = input.split("\n"); const lineStyles: LineStyle[] = []; const processedLines: string[] = []; - let activeFence: FenceMarker | null = null; + let activeFence: ActiveFence | null = null; for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { - let line = lines[lineIndex]; - let { text: unquotedLine, indent: baseIndent } = stripQuotePrefix(line); - line = unquotedLine; + const rawLine = lines[lineIndex]; + const { text: unquotedLine, indent: baseIndent } = stripQuotePrefix(rawLine); - const fence = parseFenceMarker(line); - if (fence) { - if (!activeFence) { - if (!hasClosingFence(lines, lineIndex + 1, fence)) { - processedLines.push(escapeLiteralText(line, escapeMap)); - activeFence = fence; - continue; - } - activeFence = fence; - continue; - } - - if (isClosingFence(line, activeFence)) { + if (activeFence) { + const codeLine = activeFence.quoteIndent > 0 ? unquotedLine : rawLine; + if (isClosingFence(codeLine, activeFence)) { activeFence = null; continue; } - } - - if (activeFence) { processedLines.push( escapeLiteralText( - normalizeCodeBlockLeadingWhitespace(stripCodeFenceIndent(line, activeFence.indent)), + normalizeCodeBlockLeadingWhitespace(stripCodeFenceIndent(codeLine, activeFence.indent)), escapeMap, ), ); continue; } + let line = unquotedLine; + const openingFence = resolveOpeningFence(rawLine); + if (openingFence) { + const fenceLine = openingFence.quoteIndent > 0 ? unquotedLine : rawLine; + if (!hasClosingFence(lines, lineIndex + 1, openingFence)) { + processedLines.push(escapeLiteralText(fenceLine, escapeMap)); + activeFence = openingFence; + continue; + } + activeFence = openingFence; + continue; + } + const headingMatch = line.match(/^(#{1,4})\s(.*)$/); if (headingMatch) { const outputLineIndex = processedLines.length; @@ -288,15 +291,38 @@ function clampIndent(spaceCount: number): number { return Math.min(5, Math.max(1, Math.floor(spaceCount / 2))); } -function hasClosingFence(lines: string[], startIndex: number, fence: FenceMarker): boolean { +function hasClosingFence(lines: string[], startIndex: number, fence: ActiveFence): boolean { for (let index = startIndex; index < lines.length; index += 1) { - if (isClosingFence(stripQuotePrefix(lines[index]).text, fence)) { + const candidate = fence.quoteIndent > 0 ? stripQuotePrefix(lines[index]).text : lines[index]; + if (isClosingFence(candidate, fence)) { return true; } } return false; } +function resolveOpeningFence(line: string): ActiveFence | null { + const directFence = parseFenceMarker(line); + if (directFence) { + return { ...directFence, quoteIndent: 0 }; + } + + const quoted = stripQuotePrefix(line); + if (quoted.indent === 0) { + return null; + } + + const quotedFence = parseFenceMarker(quoted.text); + if (!quotedFence) { + return null; + } + + return { + ...quotedFence, + quoteIndent: quoted.indent, + }; +} + function stripQuotePrefix(line: string): { text: string; indent: number } { const match = line.match(/^(>+)\s?(.*)$/); if (!match) {