From 1c323351a2a5df24fe200d768f797098068a63e1 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 8 Jun 2026 13:31:26 +0000 Subject: [PATCH] fix: parse Quarto schema YAML errors and stop heading style bleeding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two unrelated fixes: 1. quarto-log-parser: handle the two-line Quarto schema-validation error format: ERROR: In file main.qmd (line 6, columns 24--27) Field "section-numbering" has value … Previously neither the file name nor the line number were extracted, so the error appeared without a red highlight. Now the first line stores the filename in pendingLocation and the second line creates the log entry with the correct file and line so the editor can jump to and highlight it. 2. headingTitleTokenizer: change contextual: false → contextual: true and guard with stack.canShift(HeadingTitle). With contextual: false Lezer calls the tokenizer speculatively at positions beyond the strict post-HeadingMark state; in some LALR-merged states the resulting token was accepted for body-text lines, making them render as bold-blue heading text. The contextual guard ensures the tokenizer only fires in the one state where HeadingTitle is legitimately valid. Co-Authored-By: Claude Sonnet 4.6 --- .../source-editor/lezer-typst/tokens.mjs | 12 ++++- .../js/ide/log-parser/quarto-log-parser.ts | 44 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs b/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs index bdcadf8e1a..26710b6ed3 100644 --- a/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs +++ b/services/web/frontend/js/features/source-editor/lezer-typst/tokens.mjs @@ -56,8 +56,16 @@ export const headingTokenizer = new ExternalTokenizer( // Using an external token (terminal) instead of grammar sub-items avoids // LALR(1) conflicts: any token shared between headingTitleItem and document- // level item causes the automaton to merge the two contexts. +// +// contextual: true — the tokenizer checks stack.canShift(HeadingTitle) before +// doing anything, so it ONLY fires in the LR state reached after HeadingMark. +// With contextual: false, Lezer calls the tokenizer speculatively at any +// position where HeadingTitle is *conceivably* valid (due to LALR state +// merging), which can cause whole lines of body text to be consumed as +// HeadingTitle tokens and styled as headings. export const headingTitleTokenizer = new ExternalTokenizer( - (input, _stack) => { + (input, stack) => { + if (!stack.canShift(HeadingTitle)) return let hasContent = false while (input.next !== -1 && input.next !== NEWLINE) { // Stop before a line comment (//) or block comment (/*) so that @@ -69,7 +77,7 @@ export const headingTitleTokenizer = new ExternalTokenizer( } if (hasContent) input.acceptToken(HeadingTitle) }, - { contextual: false } + { contextual: true } ) // ── rawTokenizer ──────────────────────────────────────────────────────── diff --git a/services/web/frontend/js/ide/log-parser/quarto-log-parser.ts b/services/web/frontend/js/ide/log-parser/quarto-log-parser.ts index a1dbdfada0..3078285a17 100644 --- a/services/web/frontend/js/ide/log-parser/quarto-log-parser.ts +++ b/services/web/frontend/js/ide/log-parser/quarto-log-parser.ts @@ -58,6 +58,11 @@ const PY_MODULE_TO_PACKAGE: Record = { } // A typst diagnostic location line: ` ┌─ main.typ:5:10` / ` --> main.typ:5:10` const TYPST_LOCATION_REGEX = /(?:[┌╭]─|-->)\s*(.+?):(\d+):(\d+)/ +// Quarto schema-validation location line emitted after `ERROR: In file `: +// (line 6, columns 24--27) Field "section-numbering" has value … +const QUARTO_SCHEMA_LOC_REGEX = /^\(line (\d+), columns? \d+(?:--\d+)?\)\s+(.*)/ +// Extracts the filename from an `ERROR: In file ` message. +const QUARTO_IN_FILE_REGEX = /^In file (.+)$/ function stripAnsi(line: string): string { return line.replace(ANSI_REGEX, '') @@ -67,7 +72,8 @@ function isDiagnosticStart(trimmed: string): boolean { return ( LOWER_DIAG_REGEX.test(trimmed) || UPPER_DIAG_REGEX.test(trimmed) || - PANDOC_REGEX.test(trimmed) + PANDOC_REGEX.test(trimmed) || + QUARTO_SCHEMA_LOC_REGEX.test(trimmed) ) } @@ -119,14 +125,48 @@ export default function parseQuartoLog(rawLog: string): ParseResult { continue } + // Quarto schema-validation errors span two lines: + // ERROR: In file main.qmd ← stores pendingLocation.file + // (line 6, columns 24--27) Field … ← this line carries line + message + let m: RegExpMatchArray | null + if ((m = trimmed.match(QUARTO_SCHEMA_LOC_REGEX))) { + const logLine = parseInt(m[1], 10) + const msg = m[2].trim() + let content = clean + let j = i + 1 + for (; j < lines.length; j++) { + const next = stripAnsi(lines[j]) + if (next.trim() === '') break + if (isDiagnosticStart(next.trimStart())) break + content += '\n' + next + } + i = j - 1 + data.push({ + line: logLine, + file: pendingLocation.file, + level: 'error', + message: msg, + content, + raw: content, + }) + pendingLocation = {} + continue + } + let level: LatexLogEntry['level'] | null = null let message: string | null = null - let m: RegExpMatchArray | null if ((m = trimmed.match(LOWER_DIAG_REGEX))) { level = m[1] === 'error' ? 'error' : 'warning' message = m[2] } else if ((m = trimmed.match(UPPER_DIAG_REGEX))) { + // `ERROR: In file ` is a preamble for a schema-validation error; + // store the filename and defer the entry to the (line N, …) line that follows. + const inFile = m[2].trim().match(QUARTO_IN_FILE_REGEX) + if (inFile) { + pendingLocation = { ...pendingLocation, file: inFile[1].trim() } + continue + } level = m[1] === 'ERROR' ? 'error' : 'warning' message = m[2] } else if ((m = trimmed.match(PANDOC_REGEX))) {