fix(zalouser): preserve literal quote lines in code fences

This commit is contained in:
Tuyen
2026-03-12 00:57:02 +07:00
parent 635f9734e0
commit 3f7565abcd
2 changed files with 63 additions and 23 deletions

View File

@@ -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",

View File

@@ -34,6 +34,10 @@ type FenceMarker = {
indent: number;
};
type ActiveFence = FenceMarker & {
quoteIndent: number;
};
const TAG_STYLE_MAP: Record<string, InlineStyle | null> = {
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) {