fix: parse Quarto schema YAML errors and stop heading style bleeding
Build and Deploy Verso / deploy (push) Successful in 9m46s
Build and Deploy Verso / deploy (push) Successful in 9m46s
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
55e8208892
commit
1c323351a2
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -58,6 +58,11 @@ const PY_MODULE_TO_PACKAGE: Record<string, string> = {
|
||||
}
|
||||
// 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 <f>`:
|
||||
// (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 <filename>` 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 <filename>` 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))) {
|
||||
|
||||
Reference in New Issue
Block a user