From 3aebce4aa8037d9c3d8e1cec298ed27bd1e9b69f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 9 Jul 2026 15:46:39 +0200 Subject: [PATCH 01/17] Bump Pandoc to 3.10 and Typst to 0.15.0 in configuration Version bump only, so the new binaries can be archived to S3 and downloaded during configure. Template resync, Lua API checks, and test coverage work are tracked separately and land in follow-up commits before this is considered a complete upgrade. --- configuration | 4 ++-- src/command/check/check.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration b/configuration index e70824da12..411bc53bf6 100644 --- a/configuration +++ b/configuration @@ -13,10 +13,10 @@ export DENO=v2.7.14 # TODO figure out where 0.1.41 apple silicon libs are available export DENO_DOM=v0.1.41-alpha-artifacts -export PANDOC=3.8.3 +export PANDOC=3.10 export DARTSASS=1.87.0 export ESBUILD=0.25.10 -export TYPST=0.14.2 +export TYPST=0.15.0 export TYPST_GATHER=0.2.3 export VERAPDF=1.28.2 diff --git a/src/command/check/check.ts b/src/command/check/check.ts index dce5c9c8bb..bf0dec093c 100644 --- a/src/command/check/check.ts +++ b/src/command/check/check.ts @@ -244,10 +244,10 @@ async function checkVersions(conf: CheckConfiguration) { // file is in an awkward format and it is not packaged // with our installers const versionConstraints: [string | undefined, string, string][] = [ - [pandocVersion, "3.8.3", "Pandoc"], + [pandocVersion, "3.10", "Pandoc"], [sassVersion, "1.87.0", "Dart Sass"], [denoVersion, "2.7.14", "Deno"], - [typstVersion, "0.14.2", "Typst"], + [typstVersion, "0.15.0", "Typst"], ]; const checkData: [string | undefined, string, string][] = versionConstraints .map(([version, ver, name]) => [ From f33a6e61a6161bfad231057678782223b85f2323 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 9 Jul 2026 18:03:14 +0200 Subject: [PATCH 02/17] Bump Dart Sass to 1.101.0, esbuild to 0.28.1, veraPDF to 1.30.2 Riding along in the same S3-publish branch as the Pandoc/Typst bump, since Carlos is already archiving from this branch. Version bump only, no risk analysis done yet for these three, same as Pandoc and Typst this is prep for archiving, not a verified upgrade. --- configuration | 6 +++--- src/command/check/check.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration b/configuration index 411bc53bf6..9c200155b2 100644 --- a/configuration +++ b/configuration @@ -14,11 +14,11 @@ export DENO=v2.7.14 # TODO figure out where 0.1.41 apple silicon libs are available export DENO_DOM=v0.1.41-alpha-artifacts export PANDOC=3.10 -export DARTSASS=1.87.0 -export ESBUILD=0.25.10 +export DARTSASS=1.101.0 +export ESBUILD=0.28.1 export TYPST=0.15.0 export TYPST_GATHER=0.2.3 -export VERAPDF=1.28.2 +export VERAPDF=1.30.2 # NB: we can't put comments in the same line as export statements because it diff --git a/src/command/check/check.ts b/src/command/check/check.ts index bf0dec093c..2dad545564 100644 --- a/src/command/check/check.ts +++ b/src/command/check/check.ts @@ -245,7 +245,7 @@ async function checkVersions(conf: CheckConfiguration) { // with our installers const versionConstraints: [string | undefined, string, string][] = [ [pandocVersion, "3.10", "Pandoc"], - [sassVersion, "1.87.0", "Dart Sass"], + [sassVersion, "1.101.0", "Dart Sass"], [denoVersion, "2.7.14", "Deno"], [typstVersion, "0.15.0", "Typst"], ]; From 2ef072bed109d15ede8645a54f00da81a556ff2e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 10 Jul 2026 16:35:51 +0200 Subject: [PATCH 03/17] Update all templates with pandoc 3.10 changes --- .../formats/beamer/pandoc/beamer.template | 2 +- .../formats/beamer/pandoc/hypersetup.latex | 6 +- src/resources/formats/beamer/pandoc/title.tex | 2 +- src/resources/formats/html/pandoc/html.styles | 10 ++- .../formats/html/pandoc/html.template | 4 +- src/resources/formats/html/pandoc/styles.html | 10 ++- .../formats/html/pandoc/template.html | 2 +- .../formats/pdf/pandoc/hypersetup.latex | 6 +- .../formats/pdf/pandoc/latex.template | 1 + .../formats/revealjs/pandoc/revealjs.template | 25 +++++- .../formats/revealjs/pandoc/template.html | 9 +++ .../formats/typst/pandoc/template.typst | 79 ++++++++++--------- .../formats/typst/pandoc/typst.template | 12 +-- 13 files changed, 105 insertions(+), 63 deletions(-) diff --git a/src/resources/formats/beamer/pandoc/beamer.template b/src/resources/formats/beamer/pandoc/beamer.template index 6e2c0ed80c..bb88d82662 100644 --- a/src/resources/formats/beamer/pandoc/beamer.template +++ b/src/resources/formats/beamer/pandoc/beamer.template @@ -125,7 +125,7 @@ $for(titlegraphic)$ $endfor$} $endif$ $if(logo)$ -\logo{\includegraphics{$logo$}} +\logo{\includegraphics$if(logooptions)$[$for(logooptions)$$logooptions$$sep$, $endfor$]$endif${$logo$}} $endif$ \begin{document} diff --git a/src/resources/formats/beamer/pandoc/hypersetup.latex b/src/resources/formats/beamer/pandoc/hypersetup.latex index ff67655576..610ba2c8fd 100644 --- a/src/resources/formats/beamer/pandoc/hypersetup.latex +++ b/src/resources/formats/beamer/pandoc/hypersetup.latex @@ -1,3 +1,7 @@ +% fallback for those not using the hyperref driver hyperxmp: +\makeatletter +\@ifundefined{xmpquote}{\newcommand{\xmpquote}[1]{#1}}{} +\makeatother \hypersetup{ $if(title-meta)$ pdftitle={$title-meta$}, @@ -12,7 +16,7 @@ $if(subject)$ pdfsubject={$subject$}, $endif$ $if(keywords)$ - pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$}, + pdfkeywords={$for(keywords)$\xmpquote{$keywords$}$sep$, $endfor$}, $endif$ $if(colorlinks)$ colorlinks=true, diff --git a/src/resources/formats/beamer/pandoc/title.tex b/src/resources/formats/beamer/pandoc/title.tex index a3163a911c..ccbe5198f1 100644 --- a/src/resources/formats/beamer/pandoc/title.tex +++ b/src/resources/formats/beamer/pandoc/title.tex @@ -18,5 +18,5 @@ $endfor$} $endif$ $if(logo)$ -\logo{\includegraphics{$logo$}} +\logo{\includegraphics$if(logooptions)$[$for(logooptions)$$logooptions$$sep$, $endfor$]$endif${$logo$}} $endif$ \ No newline at end of file diff --git a/src/resources/formats/html/pandoc/html.styles b/src/resources/formats/html/pandoc/html.styles index 800f63c7a6..cac6172c5e 100644 --- a/src/resources/formats/html/pandoc/html.styles +++ b/src/resources/formats/html/pandoc/html.styles @@ -106,6 +106,7 @@ div.abstract-title { } $endif$ code { + white-space: pre-wrap; font-family: $if(monofont)$$monofont$$else$Menlo, Monaco, Consolas, 'Lucida Console', monospace$endif$; $if(monobackgroundcolor)$ background-color: $monobackgroundcolor$; @@ -183,10 +184,13 @@ header { text-decoration: none; } $endif$ -code{white-space: pre-wrap;} span.smallcaps{font-variant: small-caps;} -div.columns{display: flex; gap: min(4vw, 1.5em);} -div.column{flex: auto; overflow-x: auto;} +div.columns{display: flex; gap: 1.5em;} +div.column{flex: auto;} +@media screen { +div.columns{gap: min(4vw, 1.5em);} +div.column{overflow-x: auto;} +} div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} /* The extra [class] is a hack that increases specificity enough to override a similar rule in reveal.js */ diff --git a/src/resources/formats/html/pandoc/html.template b/src/resources/formats/html/pandoc/html.template index 26740d1b0e..2f39bc95ec 100644 --- a/src/resources/formats/html/pandoc/html.template +++ b/src/resources/formats/html/pandoc/html.template @@ -1,8 +1,8 @@ - + - + $for(author-meta)$ diff --git a/src/resources/formats/html/pandoc/styles.html b/src/resources/formats/html/pandoc/styles.html index ebcc014be1..795fd31130 100644 --- a/src/resources/formats/html/pandoc/styles.html +++ b/src/resources/formats/html/pandoc/styles.html @@ -118,6 +118,7 @@ } $endif$ code { + white-space: pre-wrap; font-family: $if(monofont)$$monofont$$else$Menlo, Monaco, Consolas, 'Lucida Console', monospace$endif$; $if(monobackgroundcolor)$ background-color: $monobackgroundcolor$; @@ -190,10 +191,13 @@ text-decoration: none; } $endif$ -code{white-space: pre-wrap;} span.smallcaps{font-variant: small-caps;} -div.columns{display: flex; gap: min(4vw, 1.5em);} -div.column{flex: auto; overflow-x: auto;} +div.columns{display: flex; gap: 1.5em;} +div.column{flex: auto;} +@media screen { +div.columns{gap: min(4vw, 1.5em);} +div.column{overflow-x: auto;} +} div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} ul.task-list{list-style: none;} ul.task-list li input[type="checkbox"] { diff --git a/src/resources/formats/html/pandoc/template.html b/src/resources/formats/html/pandoc/template.html index 3a7b1b9675..5ae339e921 100644 --- a/src/resources/formats/html/pandoc/template.html +++ b/src/resources/formats/html/pandoc/template.html @@ -1,5 +1,5 @@ - + diff --git a/src/resources/formats/pdf/pandoc/hypersetup.latex b/src/resources/formats/pdf/pandoc/hypersetup.latex index ff67655576..610ba2c8fd 100644 --- a/src/resources/formats/pdf/pandoc/hypersetup.latex +++ b/src/resources/formats/pdf/pandoc/hypersetup.latex @@ -1,3 +1,7 @@ +% fallback for those not using the hyperref driver hyperxmp: +\makeatletter +\@ifundefined{xmpquote}{\newcommand{\xmpquote}[1]{#1}}{} +\makeatother \hypersetup{ $if(title-meta)$ pdftitle={$title-meta$}, @@ -12,7 +16,7 @@ $if(subject)$ pdfsubject={$subject$}, $endif$ $if(keywords)$ - pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$}, + pdfkeywords={$for(keywords)$\xmpquote{$keywords$}$sep$, $endfor$}, $endif$ $if(colorlinks)$ colorlinks=true, diff --git a/src/resources/formats/pdf/pandoc/latex.template b/src/resources/formats/pdf/pandoc/latex.template index 6c70459554..e993af1d77 100644 --- a/src/resources/formats/pdf/pandoc/latex.template +++ b/src/resources/formats/pdf/pandoc/latex.template @@ -1,3 +1,4 @@ +$document-metadata.latex()$ $passoptions.latex()$ \documentclass[ $for(babel-otherlangs)$ diff --git a/src/resources/formats/revealjs/pandoc/revealjs.template b/src/resources/formats/revealjs/pandoc/revealjs.template index 5278dd1457..0fc4464f17 100644 --- a/src/resources/formats/revealjs/pandoc/revealjs.template +++ b/src/resources/formats/revealjs/pandoc/revealjs.template @@ -29,6 +29,9 @@ $if(theme)$ $else$ $endif$ +$if(highlight-js)$ + +$endif$ $for(css)$ $endfor$ @@ -87,6 +90,9 @@ $body$ $if(mathjax)$ $endif$ +$if(highlight-js)$ + +$endif$ $if(mathjax)$ +$endif$ +$if(highlight-js)$ + $endif$ @@ -313,6 +319,9 @@ plugins: [ $if(mathjax)$ RevealMath, +$endif$ +$if(highlight-js)$ + RevealHighlight, $endif$ RevealNotes, RevealSearch, diff --git a/src/resources/formats/typst/pandoc/template.typst b/src/resources/formats/typst/pandoc/template.typst index 648476620f..1d90b1d976 100644 --- a/src/resources/formats/typst/pandoc/template.typst +++ b/src/resources/formats/typst/pandoc/template.typst @@ -73,47 +73,48 @@ } } - block(below: 1em, width: 100%)[ - #if title != none { - align(center, block[ - #text(weight: "bold", size: 1.5em)[#title #if thanks != none { - footnote(thanks, numbering: "*") - counter(footnote).update(n => n - 1) - }] - #( - if subtitle != none { - parbreak() - text(weight: "bold", size: 1.25em)[#subtitle] - } - )]) - } - - #if authors != none and authors != [] { - let count = authors.len() - let ncols = calc.min(count, 3) - grid( - columns: (1fr,) * ncols, - row-gutter: 1.5em, - ..authors.map(author => align(center)[ - #author.name \ - #author.affiliation \ - #author.email - ]) - ) - } + if title != none { + place(top, float: true, scope: "parent", clearance: 4mm, block(below: 1em, width: 100%)[ + #if title != none { + align(center, block[ + #text(weight: "bold", size: 1.5em, hyphenate: false)[#title #if thanks != none { + footnote(thanks, numbering: "*") + counter(footnote).update(n => n - 1) + }] + #( + if subtitle != none { + parbreak() + text(weight: "bold", size: 1.25em, hyphenate: false)[#subtitle] + } + )]) + } - #if date != none { - align(center)[#block(inset: 1em)[ - #date - ]] - } + #if authors != none and authors != [] { + let count = authors.len() + let ncols = calc.min(count, 3) + grid( + columns: (1fr,) * ncols, + row-gutter: 1.5em, + ..authors.map(author => align(center)[ + #author.name \ + #author.affiliation \ + #author.email + ]) + ) + } - #if abstract != none { - block(inset: 2em)[ - #text(weight: "semibold")[#abstract-title] #h(1em) #abstract - ] - } - ] + #if date != none { + align(center)[#block(inset: 1em)[ + #date + ]] + } + #if abstract != none { + block(inset: 2em)[ + #text(weight: "semibold")[#abstract-title] #h(1em) #abstract + ] + } + ]) + } doc } diff --git a/src/resources/formats/typst/pandoc/typst.template b/src/resources/formats/typst/pandoc/typst.template index a499cd8db9..9b161a4c87 100644 --- a/src/resources/formats/typst/pandoc/typst.template +++ b/src/resources/formats/typst/pandoc/typst.template @@ -1,13 +1,9 @@ #let horizontalrule = line(start: (25%,0%), end: (75%,0%)) -#show terms: it => { - it.children - .map(child => [ - #strong[#child.term] - #block(inset: (left: 1.5em, top: -0.4em))[#child.description] - ]) - .join() -} +#show terms.item: it => block(breakable: false)[ + #text(weight: "bold")[#it.term] + #block(inset: (left: 1.5em, top: -0.4em))[#it.description] +] #set table( inset: 6pt, From 3d812e07c0517c8ac6cac62a9295868074a59fa7 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 10 Jul 2026 16:47:41 +0200 Subject: [PATCH 04/17] Fix jog.lua traverser for pandoc 3.10's typed TableBody Pandoc 3.10 upgraded TableBody to a proper typed Lua object (matching TableHead/TableFoot, which jog.lua already handled), so the untyped table fallback branch no longer catches it and every table triggered "Don't know how to traverse TableBody" during render. --- src/resources/filters/modules/jog.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/resources/filters/modules/jog.lua b/src/resources/filters/modules/jog.lua index f3ab5c0730..a3547570c4 100644 --- a/src/resources/filters/modules/jog.lua +++ b/src/resources/filters/modules/jog.lua @@ -118,6 +118,9 @@ local function recurse (element, tp, jogger) elseif tp == 'pandoc TableHead' or tp == 'pandoc TableFoot' or tp == 'TableHead' or tp == 'TableFoot' then element.rows = jogger(element.rows) + elseif tp == 'pandoc TableBody' or tp == 'TableBody' then + element.head = jogger(element.head) + element.body = jogger(element.body) elseif tp == 'Blocks' or tp == 'Inlines' then local expected_itemtype = tp == 'Inlines' and 'Inline' or 'Block' local pos = 0 From 646a0cd83a37982940ae3bef633bd697abdab932 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 10 Jul 2026 16:56:04 +0200 Subject: [PATCH 05/17] Update Lua type stubs for pandoc 3.9-3.10 additive API changes pandoc.readers/writers values changed from boolean to string ("text"/"bytestring") in pandoc 3.9; add missing pandoc.TableBody constructor and pandoc.types.Sources/Source stubs. --- src/resources/lua-types/pandoc/components.lua | 10 ++++++++ src/resources/lua-types/pandoc/pandoc.lua | 4 ++-- src/resources/lua-types/pandoc/version.lua | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/resources/lua-types/pandoc/components.lua b/src/resources/lua-types/pandoc/components.lua index 7c83870859..06b1e65a77 100644 --- a/src/resources/lua-types/pandoc/components.lua +++ b/src/resources/lua-types/pandoc/components.lua @@ -198,6 +198,16 @@ number of row header columns. pandoc.TableBody = {} +--[[ +Creates a table body +]] +---@param body? pandoc.List List of `Row` +---@param head? pandoc.List Intermediate head (list of `Row`) +---@param row_head_columns? integer Number of columns taken up by the row head of each row of a `TableBody` +---@param attr? pandoc.Attr Table body attributes +---@return pandoc.TableBody +function pandoc.TableBody(body, head, row_head_columns, attr) end + --[[ Make a clone ]] diff --git a/src/resources/lua-types/pandoc/pandoc.lua b/src/resources/lua-types/pandoc/pandoc.lua index 0c33cb5e45..cea7c64e34 100644 --- a/src/resources/lua-types/pandoc/pandoc.lua +++ b/src/resources/lua-types/pandoc/pandoc.lua @@ -3,10 +3,10 @@ ---@module 'pandoc' pandoc = {} ----@type table +---@type table pandoc.readers = {} ----@type table +---@type table pandoc.writers = {} --[[ diff --git a/src/resources/lua-types/pandoc/version.lua b/src/resources/lua-types/pandoc/version.lua index ecf1c5394a..89c8869159 100644 --- a/src/resources/lua-types/pandoc/version.lua +++ b/src/resources/lua-types/pandoc/version.lua @@ -27,6 +27,30 @@ pandoc.Version = {} function pandoc.types.Version(version_specifier) end +--[[ +A named source of text, e.g. a file name paired with its contents. +Used as an item in a `Sources` list. +]] +---@class pandoc.types.Source +---@field name string Name/path the text originated from +---@field text string The source text + +--[[ +A list of `Source` items, pairing input text with information on +where it originated (e.g. a file name). Used by Pandoc's text readers. +]] +---@class pandoc.types.Sources: pandoc.List + +---@alias sources_specifier string|table|pandoc.types.Source[]|pandoc.types.Sources + +--[[ +Creates a new Sources element, i.e., a list of `Source` items. +]] +---@param srcs sources_specifier +---@return pandoc.types.Sources +function pandoc.types.Sources(srcs) end + + --[[ Raise an error message if the version is older than the expected version; does nothing if actual is equal to or newer than the expected version. From 57a2991f479ef1b377f6855728b4448fc4552a4a Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 10 Jul 2026 17:21:13 +0200 Subject: [PATCH 06/17] Fix mediabag-routed image paths breaking Typst 0.15 compile on Windows Typst 0.15.0 now hard-rejects backslash path separators in image() calls. pandoc.path.join uses the host OS's native separator, producing backslash-joined paths on Windows for any mediabag-routed image (data URI, remote, or resource-path). Normalize to forward slashes before assigning as the Image element's src. --- src/resources/filters/modules/mediabag.lua | 5 +- tests/smoke/lua-unit/lua-unit.test.ts | 1 + tests/unit-lua/mediabag.test.lua | 57 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/unit-lua/mediabag.test.lua diff --git a/src/resources/filters/modules/mediabag.lua b/src/resources/filters/modules/mediabag.lua index 658b613b15..5662ebeac7 100644 --- a/src/resources/filters/modules/mediabag.lua +++ b/src/resources/filters/modules/mediabag.lua @@ -82,7 +82,10 @@ local function write_mediabag_entry(src) if contents == nil then return nil end local mediabagDir = param("mediabag-dir", nil) - local mediaFile = pandoc.path.join{mediabagDir, src} + -- Always use forward slashes: this path is assigned to el.src and can flow + -- into writers (e.g. Typst 0.15+) that reject backslash path separators, + -- while forward slashes work fine for the actual file write on Windows too. + local mediaFile = pandoc.path.join{mediabagDir, src}:gsub('\\', '/') local file = _quarto.file.write(mediaFile, contents) if not file then diff --git a/tests/smoke/lua-unit/lua-unit.test.ts b/tests/smoke/lua-unit/lua-unit.test.ts index 1553052f60..5cc15696e3 100644 --- a/tests/smoke/lua-unit/lua-unit.test.ts +++ b/tests/smoke/lua-unit/lua-unit.test.ts @@ -25,6 +25,7 @@ import { unitTest } from "../../test.ts"; // Explicit list, relative to tests/unit-lua/. Keep alphabetized. const LUA_TESTS: string[] = [ + "mediabag.test.lua", "typst-css.test.lua", ]; diff --git a/tests/unit-lua/mediabag.test.lua b/tests/unit-lua/mediabag.test.lua new file mode 100644 index 0000000000..8a00d546c2 --- /dev/null +++ b/tests/unit-lua/mediabag.test.lua @@ -0,0 +1,57 @@ +-- Unit tests for src/resources/filters/modules/mediabag.lua +-- +-- Run via the Deno smoke test in tests/smoke/lua-unit/lua-unit.test.ts +-- (which sets LUA_PATH and invokes `quarto run`), or directly with: +-- LUA_PATH="tests/unit-lua/?.lua;src/resources/filters/modules/?.lua;;" \ +-- package/dist/bin/quarto run tests/unit-lua/mediabag.test.lua + +-- Mocks for filter-runtime globals ------------------------------------------- + +local param_values = {} +function param(name, default) + if param_values[name] ~= nil then return param_values[name] end + return default +end + +function warn(_msg) end + +_quarto = { + file = { + write = function(_path, _contents) return true end, + }, +} + +local lu = require('luaunit') +local mediabag = require('mediabag') + +-- Tests ---------------------------------------------------------------------- + +-- Regression test for Typst 0.15.0 hard-rejecting backslash path separators +-- in image() calls (quarto-cli-fpil). pandoc.path.join uses the host OS's +-- native separator, which is a backslash on Windows. Any path handed to +-- el.src must use forward slashes only, since it flows straight into +-- writers (e.g. Typst's image()) that may not accept native separators. +TestWriteMediabagEntry = {} + +function TestWriteMediabagEntry:setUp() + param_values = { ["mediabag-dir"] = "imgtest_files/mediabag" } + pandoc.mediabag.insert("png-base64,abc123.png", "image/png", "fake-bytes") +end + +function TestWriteMediabagEntry:tearDown() + pandoc.mediabag.empty() +end + +function TestWriteMediabagEntry:testResultHasNoBackslash() + local path = mediabag.write_mediabag_entry("png-base64,abc123.png") + lu.assertNotNil(path) + lu.assertNil(path:find('\\', 1, true), + 'write_mediabag_entry returned a path with a backslash: ' .. path) +end + +function TestWriteMediabagEntry:testResultJoinsDirAndFilename() + local path = mediabag.write_mediabag_entry("png-base64,abc123.png") + lu.assertEquals(path, "imgtest_files/mediabag/png-base64,abc123.png") +end + +os.exit(lu.LuaUnit.run()) From e8921e613704bc8c906795f327469815e069fab3 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 14:23:47 +0200 Subject: [PATCH 07/17] Fix pandoc 3.10 test regressions: version check + output-shape drift Pandoc 3.10 normalizes to a 3-part version string, so the strict-mode comparison in check.ts needs to match "3.10.0" not "3.10". The other fixtures assert on cosmetic pandoc 3.10 typst/asciidoc writer output-shape changes (newline after raw #set text() inlines, backslash- escaped leading periods/minus signs after inline markup close brackets, an added alt argument on image() calls, and double-underscore emphasis in asciidoc) rather than functional regressions -- rendered output is unchanged and valid in each case. --- src/command/check/check.ts | 2 +- tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd | 2 +- tests/docs/smoke-all/2026/06/29/14583-typst.qmd | 4 ++-- .../float/asciidoc/asciidoc-float-caption-formatting-1.qmd | 2 +- .../docs/smoke-all/typst/css-property-processing/default.qmd | 2 +- .../smoke-all/typst/css-property-processing/translate.qmd | 2 +- tests/docs/smoke-all/typst/gt-islands.qmd | 2 +- tests/docs/smoke-all/typst/pandas-cell-css-rules.qmd | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/command/check/check.ts b/src/command/check/check.ts index 2dad545564..7d18d40e07 100644 --- a/src/command/check/check.ts +++ b/src/command/check/check.ts @@ -244,7 +244,7 @@ async function checkVersions(conf: CheckConfiguration) { // file is in an awkward format and it is not packaged // with our installers const versionConstraints: [string | undefined, string, string][] = [ - [pandocVersion, "3.10", "Pandoc"], + [pandocVersion, "3.10.0", "Pandoc"], [sassVersion, "1.101.0", "Dart Sass"], [denoVersion, "2.7.14", "Deno"], [typstVersion, "0.15.0", "Typst"], diff --git a/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd b/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd index 6c1b98a8a7..00db55838c 100644 --- a/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd +++ b/tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd @@ -14,7 +14,7 @@ _quarto: - ['Proof content visible'] typst: ensureTypstFileRegexMatches: - - ['#emph\[Proof\]\. Proof content visible'] + - ['#emph\[Proof\]\\?\. Proof content visible'] - [] --- diff --git a/tests/docs/smoke-all/2026/06/29/14583-typst.qmd b/tests/docs/smoke-all/2026/06/29/14583-typst.qmd index 50fad66de6..fafd36670c 100644 --- a/tests/docs/smoke-all/2026/06/29/14583-typst.qmd +++ b/tests/docs/smoke-all/2026/06/29/14583-typst.qmd @@ -11,9 +11,9 @@ _quarto: # Typst's default-image-extension is svg. A shortcode resolving to a # .png path becomes .png.svg; the spurious .svg must be stripped, # leaving the real .png (issue #14583, mismatched-extension case). - - 'image\("assets/diagram\.png"\)' + - 'image\("assets/diagram\.png"' - - - 'image\("assets/diagram\.png\.svg"\)' + - 'image\("assets/diagram\.png\.svg"' --- Shortcode resolving to a `.png` path in a Typst document (default extension diff --git a/tests/docs/smoke-all/crossrefs/float/asciidoc/asciidoc-float-caption-formatting-1.qmd b/tests/docs/smoke-all/crossrefs/float/asciidoc/asciidoc-float-caption-formatting-1.qmd index aa91f50174..439382f2d2 100644 --- a/tests/docs/smoke-all/crossrefs/float/asciidoc/asciidoc-float-caption-formatting-1.qmd +++ b/tests/docs/smoke-all/crossrefs/float/asciidoc/asciidoc-float-caption-formatting-1.qmd @@ -6,7 +6,7 @@ _quarto: asciidoc: ensureFileRegexMatches: - - - "\\.Customers _query_" + - "\\.Customers _{1,2}query_{1,2}" - "<>" - "\\[\\[lst-customers\\]\\]" --- diff --git a/tests/docs/smoke-all/typst/css-property-processing/default.qmd b/tests/docs/smoke-all/typst/css-property-processing/default.qmd index 2d554c374f..9f8f44bd61 100644 --- a/tests/docs/smoke-all/typst/css-property-processing/default.qmd +++ b/tests/docs/smoke-all/typst/css-property-processing/default.qmd @@ -6,7 +6,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(255, 0, 255\)\); B\]' + - '\[#set text\(fill: rgb\(255, 0, 255\)\);\s*B\]' - [] --- diff --git a/tests/docs/smoke-all/typst/css-property-processing/translate.qmd b/tests/docs/smoke-all/typst/css-property-processing/translate.qmd index b5c57ed355..3cfc8703b4 100644 --- a/tests/docs/smoke-all/typst/css-property-processing/translate.qmd +++ b/tests/docs/smoke-all/typst/css-property-processing/translate.qmd @@ -8,7 +8,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(255, 0, 255\)\); B\]' + - '\[#set text\(fill: rgb\(255, 0, 255\)\);\s*B\]' - [] --- diff --git a/tests/docs/smoke-all/typst/gt-islands.qmd b/tests/docs/smoke-all/typst/gt-islands.qmd index 6f80c9ac66..61ca2751ad 100644 --- a/tests/docs/smoke-all/typst/gt-islands.qmd +++ b/tests/docs/smoke-all/typst/gt-islands.qmd @@ -11,7 +11,7 @@ _quarto: tests: typst: ensureTypstFileRegexMatches: - - ['#set text\(size: 1\.25em , weight: "regular" , fill: rgb\("#333333"\)\); Large Landmasses of the World'] + - ['#set text\(size: 1\.25em , weight: "regular" , fill: rgb\("#333333"\)\);\s*Large Landmasses of the World'] - [] --- diff --git a/tests/docs/smoke-all/typst/pandas-cell-css-rules.qmd b/tests/docs/smoke-all/typst/pandas-cell-css-rules.qmd index ccf52d0853..6ccfcf0f6c 100644 --- a/tests/docs/smoke-all/typst/pandas-cell-css-rules.qmd +++ b/tests/docs/smoke-all/typst/pandas-cell-css-rules.qmd @@ -16,8 +16,8 @@ _quarto: typst: ensureTypstFileRegexMatches: - [ - 'table.cell\(fill: rgb\("#ffc0cb"\)\)\[#set text\(fill: white\); 1.867558\]', - '\[#set text\(fill: rgb\("#ff413633"\)\); -0.151357\]' + 'table.cell\(fill: rgb\("#ffc0cb"\)\)\[#set text\(fill: white\);\s*1.867558\]', + '\[#set text\(fill: rgb\("#ff413633"\)\);\s*\\?-0.151357\]' ] - [] --- From 08deb91d71ebcbb3a500dd1cddb036b073f77a7c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 15:07:50 +0200 Subject: [PATCH 08/17] Fix Windows backslash-path leaks into Typst output for brand logos, project-relative images, and mermaid figures Typst 0.15.0 hard-rejects backslash path separators in image() calls. Several Quarto code paths build paths with the OS-native separator (backslash on Windows) and then either double the backslash (assuming Typst would tolerate it, as older Typst versions did) or leave it unescaped, both of which now break compilation on Windows: brand logo resolution (brand.ts + typst-brand-yaml.lua), project-relative image resolution (project-paths.lua), the generic Typst image writer (typst.lua), and generated figure paths shared by handlers like mermaid (handlers/base.ts). Extracted a shared `to_forward_slashes` helper (modules/path.lua) for the Lua side, mirroring the existing `pathWithForwardSlashes` TypeScript helper already used across ~50 call sites, and migrated the two pre-existing inline occurrences of this same pattern (mediabag.lua, book-links.lua) onto it instead of leaving a third copy-pasted variant. --- src/core/brand/brand.ts | 13 ++++-- src/core/handlers/base.ts | 7 +++- src/resources/filters/modules/mediabag.lua | 8 ++-- src/resources/filters/modules/path.lua | 14 +++++++ .../filters/quarto-post/typst-brand-yaml.lua | 12 ++++-- src/resources/filters/quarto-post/typst.lua | 5 ++- .../filters/quarto-pre/book-links.lua | 4 +- .../filters/quarto-pre/project-paths.lua | 5 ++- tests/smoke/lua-unit/lua-unit.test.ts | 1 + tests/unit-lua/path.test.lua | 40 +++++++++++++++++++ 10 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 src/resources/filters/modules/path.lua create mode 100644 tests/unit-lua/path.test.lua diff --git a/src/core/brand/brand.ts b/src/core/brand/brand.ts index 8cd7b7ce48..af3063e85f 100644 --- a/src/core/brand/brand.ts +++ b/src/core/brand/brand.ts @@ -39,7 +39,7 @@ import { LogoSpecifier, LogoSpecifierPathOptional, } from "../../resources/types/schema-types.ts"; -import { ensureLeadingSlash } from "../path.ts"; +import { ensureLeadingSlash, pathWithForwardSlashes } from "../path.ts"; type ProcessedBrandData = { color: Record; @@ -246,14 +246,21 @@ export class Brand { resolvePath(entry: BrandLogoResource) { const pathPrefix = relative(this.projectDir, this.brandDir); + // Always use forward slashes: this path can flow into writers (e.g. + // Typst 0.15+) that reject backslash path separators, while forward + // slashes work fine for the actual file resolution on Windows too. if (typeof entry === "string") { - return { path: isExternalPath(entry) ? entry : join(pathPrefix, entry) }; + return { + path: isExternalPath(entry) + ? entry + : pathWithForwardSlashes(join(pathPrefix, entry)), + }; } return { ...entry, path: isExternalPath(entry.path) ? entry.path - : join(pathPrefix, entry.path), + : pathWithForwardSlashes(join(pathPrefix, entry.path)), }; } diff --git a/src/core/handlers/base.ts b/src/core/handlers/base.ts index b0e6d63a00..68969660bf 100644 --- a/src/core/handlers/base.ts +++ b/src/core/handlers/base.ts @@ -79,7 +79,7 @@ import { ensureDirSync } from "../../deno_ral/fs.ts"; import { mappedStringFromFile } from "../mapped-text.ts"; import { error } from "../../deno_ral/log.ts"; import { withCriClient } from "../cri/cri.ts"; -import { normalizePath } from "../path.ts"; +import { normalizePath, pathWithForwardSlashes } from "../path.ts"; import { InvalidShortcodeError, isBlockShortcode, @@ -247,7 +247,10 @@ function makeHandlerContext( const pngName = `${prefix}${globalFigureCounter[prefix]}${extension}`; const tempName = join(context.figuresDir(), pngName); const baseDir = dirname(options.context.target.source); - const mdName = relative(baseDir, tempName); + // Always use forward slashes: this path is embedded as an image source + // and can flow into writers (e.g. Typst 0.15+) that reject backslash + // path separators, while forward slashes work fine on Windows too. + const mdName = pathWithForwardSlashes(relative(baseDir, tempName)); this.addSupporting(relative(baseDir, context.figuresDir())); diff --git a/src/resources/filters/modules/mediabag.lua b/src/resources/filters/modules/mediabag.lua index 5662ebeac7..183527e8fa 100644 --- a/src/resources/filters/modules/mediabag.lua +++ b/src/resources/filters/modules/mediabag.lua @@ -4,6 +4,7 @@ -- helpers for working with the pandoc mediabag local filenames = require 'modules/filenames' +local path = require 'modules/path' -- A cache of image urls that we've resolved into the mediabag -- keyed by {url: mediabagpath} @@ -82,10 +83,9 @@ local function write_mediabag_entry(src) if contents == nil then return nil end local mediabagDir = param("mediabag-dir", nil) - -- Always use forward slashes: this path is assigned to el.src and can flow - -- into writers (e.g. Typst 0.15+) that reject backslash path separators, - -- while forward slashes work fine for the actual file write on Windows too. - local mediaFile = pandoc.path.join{mediabagDir, src}:gsub('\\', '/') + -- This path is assigned to el.src, so it needs forward slashes. See + -- modules/path.lua. + local mediaFile = path.to_forward_slashes(pandoc.path.join{mediabagDir, src}) local file = _quarto.file.write(mediaFile, contents) if not file then diff --git a/src/resources/filters/modules/path.lua b/src/resources/filters/modules/path.lua new file mode 100644 index 0000000000..384434a7ae --- /dev/null +++ b/src/resources/filters/modules/path.lua @@ -0,0 +1,14 @@ +-- path.lua +-- Copyright (C) 2026 Posit Software, PBC + +-- Always use forward slashes: a path built with the native separator can +-- flow into writers (e.g. Typst 0.15+) that reject backslash path +-- separators, while forward slashes work fine for file resolution on +-- Windows too. +local function to_forward_slashes(path) + return path:gsub('\\', '/') +end + +return { + to_forward_slashes = to_forward_slashes +} diff --git a/src/resources/filters/quarto-post/typst-brand-yaml.lua b/src/resources/filters/quarto-post/typst-brand-yaml.lua index d4225c4904..d530d8c75e 100644 --- a/src/resources/filters/quarto-post/typst-brand-yaml.lua +++ b/src/resources/filters/quarto-post/typst-brand-yaml.lua @@ -1,3 +1,5 @@ +local path = require 'modules/path' + function render_typst_brand_yaml() if not _quarto.format.isTypstOutput() then return {} @@ -110,7 +112,8 @@ function render_typst_brand_yaml() local declImage = {} for name, image in pairs(brandLogo.images) do declImage[name] = { - path = quote_string(image.path):gsub('\\', '\\\\'), + -- Typst 0.15+ rejects backslash path separators. + path = path.to_forward_slashes(quote_string(image.path)), alt = quote_string(image.alt), } end @@ -121,7 +124,8 @@ function render_typst_brand_yaml() for _, size in pairs({'small', 'medium', 'large'}) do if brandLogo[size] then declLogo[size] = { - path = quote_string(brandLogo[size].path):gsub('\\', '\\\\'), + -- Typst 0.15+ rejects backslash path separators. + path = path.to_forward_slashes(quote_string(brandLogo[size].path)), alt = quote_string(brandLogo[size].alt), } end @@ -328,8 +332,8 @@ function render_typst_brand_yaml() imageFilename = _quarto.modules.mediabag.write_mediabag_entry(imageFilename) or imageFilename imageFilename = imageFilename and imageFilename:gsub('\\_', '_') else - -- backslashes need to be doubled for Windows - imageFilename = string.gsub(imageFilename, '\\', '\\\\') + -- Typst 0.15+ rejects backslash path separators. + imageFilename = path.to_forward_slashes(imageFilename) end logoOptions.path = pandoc.RawInline('typst', imageFilename) meta.logo = logoOptions diff --git a/src/resources/filters/quarto-post/typst.lua b/src/resources/filters/quarto-post/typst.lua index 10e822405e..7639e84fc8 100644 --- a/src/resources/filters/quarto-post/typst.lua +++ b/src/resources/filters/quarto-post/typst.lua @@ -8,6 +8,7 @@ local typst = require("modules/typst") _quarto.format.typst = typst +local path = require("modules/path") -- Helper to format marginalia shift parameter -- auto/true/false are unquoted, "avoid"/"ignore" are quoted strings @@ -252,8 +253,8 @@ function render_typst_fixups() -- Build image() parameters local params = {} - -- Source path (escape backslashes for Windows paths) - src = src:gsub('\\', '\\\\') + -- Typst 0.15+ rejects backslash path separators in image() calls. + src = path.to_forward_slashes(src) table.insert(params, '"' .. src .. '"') -- Alt text second (escape backslashes and quotes) diff --git a/src/resources/filters/quarto-pre/book-links.lua b/src/resources/filters/quarto-pre/book-links.lua index 41e19222d6..b080b88e2f 100644 --- a/src/resources/filters/quarto-pre/book-links.lua +++ b/src/resources/filters/quarto-pre/book-links.lua @@ -1,6 +1,8 @@ -- book-links.lua -- Copyright (C) 2020-2022 Posit Software, PBC +local path = require 'modules/path' + function index_book_file_targets() if not param("single-file-book", false) then return {} @@ -53,7 +55,7 @@ function resolve_book_file_targets() package.config:sub(1,1) -- Paths are always using '/' separator (even on windows) - linkTarget = linkTarget:gsub("\\", "/") + linkTarget = path.to_forward_slashes(linkTarget) local sectionId = quarto_global_state.fileSectionIds[linkTarget]; if sectionId ~= nil then el.target = '#' .. sectionId diff --git a/src/resources/filters/quarto-pre/project-paths.lua b/src/resources/filters/quarto-pre/project-paths.lua index 8fb6d9053e..c3fb306ba7 100644 --- a/src/resources/filters/quarto-pre/project-paths.lua +++ b/src/resources/filters/quarto-pre/project-paths.lua @@ -2,11 +2,14 @@ -- Copyright (C) 2020-2022 Posit Software, PBC local constants = require("modules/constants") +local pathmodule = require("modules/path") local function resolveProjectPath(path) local offset = _quarto.projectOffset() if offset and path and startsWith(path, '/') then - return pandoc.path.join({offset, pandoc.text.sub(path, 2, #path)}) + -- This path can flow into writers (e.g. Typst 0.15+) that reject + -- backslash path separators, so it needs forward slashes. + return pathmodule.to_forward_slashes(pandoc.path.join({offset, pandoc.text.sub(path, 2, #path)})) else return nil end diff --git a/tests/smoke/lua-unit/lua-unit.test.ts b/tests/smoke/lua-unit/lua-unit.test.ts index 5cc15696e3..5a4f61341b 100644 --- a/tests/smoke/lua-unit/lua-unit.test.ts +++ b/tests/smoke/lua-unit/lua-unit.test.ts @@ -26,6 +26,7 @@ import { unitTest } from "../../test.ts"; // Explicit list, relative to tests/unit-lua/. Keep alphabetized. const LUA_TESTS: string[] = [ "mediabag.test.lua", + "path.test.lua", "typst-css.test.lua", ]; diff --git a/tests/unit-lua/path.test.lua b/tests/unit-lua/path.test.lua new file mode 100644 index 0000000000..a71c1aeb7d --- /dev/null +++ b/tests/unit-lua/path.test.lua @@ -0,0 +1,40 @@ +-- Unit tests for src/resources/filters/modules/path.lua +-- +-- Run via the Deno smoke test in tests/smoke/lua-unit/lua-unit.test.ts +-- (which sets LUA_PATH and invokes `quarto run`), or directly with: +-- LUA_PATH="tests/unit-lua/?.lua;src/resources/filters/modules/?.lua;;" \ +-- package/dist/bin/quarto run tests/unit-lua/path.test.lua + +local lu = require('luaunit') +local path = require('path') + +-- Tests ---------------------------------------------------------------------- + +-- Regression test for Typst 0.15.0 hard-rejecting backslash path separators +-- (quarto-cli-ooky). Any path built with the host OS's native separator +-- (backslash on Windows) must be normalized to forward slashes before it can +-- flow into writers (e.g. Typst's image()) that reject native separators. +TestToForwardSlashes = {} + +function TestToForwardSlashes:testReplacesBackslashes() + lu.assertEquals( + path.to_forward_slashes("a\\b\\c.png"), + "a/b/c.png" + ) +end + +function TestToForwardSlashes:testLeavesForwardSlashesUnchanged() + lu.assertEquals( + path.to_forward_slashes("a/b/c.png"), + "a/b/c.png" + ) +end + +function TestToForwardSlashes:testMixedSeparators() + lu.assertEquals( + path.to_forward_slashes("../../..\\media/table.jpg"), + "../../../media/table.jpg" + ) +end + +os.exit(lu.LuaUnit.run()) From dea7b3c5ea3b41035a014fa81ff667f659bc418c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 15:35:51 +0200 Subject: [PATCH 09/17] Fix Typst figure caption leaking into alt text under pandoc 3.10 Pandoc 3.10 added native alt: derivation for Typst image() calls, deriving it from the Image's caption field. Quarto already suppresses its own caption-as-alt fallback for figure images (via the _quarto_no_caption_alt attribute, added in 1ed4446d2 to fix #14107), but that suppression only stopped Quarto's own Lua from setting alt -- it left the Image's caption field populated, so when the bare Image node fell through to Pandoc's writer, Pandoc's new native derivation picked the caption right back up, reintroducing the same leak the earlier fix removed. Clear the caption before returning the bare Image so Pandoc's writer has nothing to derive alt from in that case. --- src/resources/filters/quarto-post/typst.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/resources/filters/quarto-post/typst.lua b/src/resources/filters/quarto-post/typst.lua index 7639e84fc8..6673320c77 100644 --- a/src/resources/filters/quarto-post/typst.lua +++ b/src/resources/filters/quarto-post/typst.lua @@ -275,6 +275,16 @@ function render_typst_fixups() return pandoc.RawInline("typst", "#box(image(" .. table.concat(params, ", ") .. "))") end + -- When caption-as-alt is deliberately suppressed (this Image is the + -- sole content of a Figure whose caption is rendered separately, see + -- layout/pandoc3_figure.lua) and no other alt text applies, clear the + -- caption before returning the bare Image. Otherwise Pandoc's own + -- Typst writer independently re-derives alt: from the Image's + -- caption, reintroducing the leak this suppression exists to prevent. + if no_caption_alt then + image.caption = pandoc.Inlines{} + end + return image end, Div = function(div) From d3a4452c54a9f41fb040cddc25490350bde575bf Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 15:53:48 +0200 Subject: [PATCH 10/17] Fix review findings: incomplete Typst path normalization and a misleading Lua type stub typst.lua's backslash-to-forward-slash normalization only ran inside the alt-text branch; an image with no alt text and no caption fell through to the bare Image return unnormalized, so a native-separator src reaching this path (from any source not already fixed at origin) could still break Typst 0.15+ compilation. Normalize on both paths. version.lua's sources_specifier alias advertised table as a valid input to pandoc.types.Sources, but empirically that shape silently produces an empty Sources list at runtime -- confirmed via `quarto run` against the bundled pandoc. Removed the misleading form. --- src/resources/filters/quarto-post/typst.lua | 5 +++++ src/resources/lua-types/pandoc/version.lua | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/resources/filters/quarto-post/typst.lua b/src/resources/filters/quarto-post/typst.lua index 6673320c77..d89a31eea8 100644 --- a/src/resources/filters/quarto-post/typst.lua +++ b/src/resources/filters/quarto-post/typst.lua @@ -285,6 +285,11 @@ function render_typst_fixups() image.caption = pandoc.Inlines{} end + -- Typst 0.15+ rejects backslash path separators in image() calls, and + -- this bare-Image path (no alt text) is handed to Pandoc's own Typst + -- writer, which emits image.src verbatim. + image.src = path.to_forward_slashes(image.src) + return image end, Div = function(div) diff --git a/src/resources/lua-types/pandoc/version.lua b/src/resources/lua-types/pandoc/version.lua index 89c8869159..f0d1aa8f3a 100644 --- a/src/resources/lua-types/pandoc/version.lua +++ b/src/resources/lua-types/pandoc/version.lua @@ -41,7 +41,7 @@ where it originated (e.g. a file name). Used by Pandoc's text readers. ]] ---@class pandoc.types.Sources: pandoc.List ----@alias sources_specifier string|table|pandoc.types.Source[]|pandoc.types.Sources +---@alias sources_specifier string|pandoc.types.Source[]|pandoc.types.Sources --[[ Creates a new Sources element, i.e., a list of `Source` items. From 8bc0407a9a6b7c2b2a7d61de47d1bb0e81eca042 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 18:32:32 +0200 Subject: [PATCH 11/17] Fix Typst bibliography paths with a leading dot under pandoc 3.10 Pandoc 3.10's Typst writer backslash-escapes any metadata string starting with a literal dot, so bibliography: ./refs.bib (or any ../ path) compiled to invalid Typst under 0.15 ("path must not contain a backslash"). Rewriting each bibliography path to a raw Typst inline bypasses that escaping entirely, mirroring the manual `./refs.bib`{=typst} workaround jgm suggested upstream (jgm/pandoc#11511, jgm/pandoc#11761 -- no fix landed there yet). Guarded on cite-method != citeproc: when a user opts into Typst's native --citeproc path, pandoc's own citeproc reads doc.meta.bibliography to load the bib file directly, so it must stay a plain path string there. Factored the rewrite into a reusable quarto.utils.as_raw_metadata helper (defaults its format argument to pandoc's FORMAT global) since the same escaping bug shape applies to other Typst metadata paths. --- news/changelog-1.10.md | 1 + .../quarto-pre/bibliography-formats.lua | 12 ++++++++++ src/resources/pandoc/datadir/_utils.lua | 23 +++++++++++++++++++ src/resources/pandoc/datadir/init.lua | 1 + .../typst/typst-bibliography-leading-dot.qmd | 22 ++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 tests/docs/smoke-all/typst/typst-bibliography-leading-dot.qmd diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 7e945c91fb..05f3339520 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -38,6 +38,7 @@ All changes included in 1.10: - ([#14460](https://github.com/quarto-dev/quarto-cli/issues/14460)): Fix CSS `border` and `border-color` declarations losing tokens that precede an `rgb()`/`rgba()` color (e.g. `border: 0px solid rgb(255, 0, 0)` rendering as a 2.25pt border instead of being suppressed). Also fixes: `var(--brand-NAME)` references crashing the Typst CSS translator when `NAME` contained digits (e.g. `--brand-red-50`); a crash when an `rgba()` alpha is unparseable; the `dvmin` length unit being silently rejected (a stray space in the unit table); CSS keywords like `BOLD` not matching as `bold` (CSS keywords are case-insensitive); invalid hex colors like `#fffff` being silently accepted as 2-component colors. - ([#14511](https://github.com/quarto-dev/quarto-cli/issues/14511)): Fix brand fonts downloaded for a Typst book project not being passed to `typst compile`, causing `unknown font family` warnings and fallback to Libertinus Serif. - ([#14524](https://github.com/quarto-dev/quarto-cli/issues/14524), [quarto-ext/orange-book#4](https://github.com/quarto-ext/orange-book/pull/4)): Fix orange-book Typst book running header not honoring `lang:` — chapter heading band stayed `Chapter N.` instead of the locale's word (e.g. `Chapitre N.` for `lang: fr`). Also fixes the orange-book `list-of-figure-title` / `list-of-table-title` template pipes which were silently rendering as empty strings. Consumes the `$quarto.language.*$` template-variable namespace from [#14530](https://github.com/quarto-dev/quarto-cli/pull/14530). +- ([#14664](https://github.com/quarto-dev/quarto-cli/pull/14664)): Fix Typst render failing with `path must not contain a backslash` when `bibliography` metadata used a relative path starting with a dot (e.g. `./refs.bib` or `../refs.bib`). ### `revealjs` diff --git a/src/resources/filters/quarto-pre/bibliography-formats.lua b/src/resources/filters/quarto-pre/bibliography-formats.lua index da7c632f8d..0b9c687b37 100644 --- a/src/resources/filters/quarto-pre/bibliography-formats.lua +++ b/src/resources/filters/quarto-pre/bibliography-formats.lua @@ -9,6 +9,18 @@ function bibliography_formats() doc.meta.references = pandoc.utils.references(doc) doc.meta.bibliography = nil return doc + elseif _quarto.format.isTypstOutput() and doc.meta.bibliography ~= nil + and quarto.doc.cite_method() ~= "citeproc" then + -- pandoc's Typst writer backslash-escapes any metadata string starting + -- with a literal dot (jgm/pandoc#11511). Rewriting each bibliography + -- path as a raw typst inline bypasses that escaping entirely. + -- + -- Skip this when citeproc is handling citations natively (cite-method + -- "citeproc", set when the user opts into `citeproc: true` for Typst): + -- pandoc's own citeproc reads doc.meta.bibliography to load the actual + -- bib file, so it must stay a plain path string here. + doc.meta.bibliography = quarto.utils.as_raw_metadata(doc.meta.bibliography) + return doc end end } diff --git a/src/resources/pandoc/datadir/_utils.lua b/src/resources/pandoc/datadir/_utils.lua index 2ea4b0580c..8a204fbaef 100644 --- a/src/resources/pandoc/datadir/_utils.lua +++ b/src/resources/pandoc/datadir/_utils.lua @@ -607,6 +607,28 @@ local function walk(node, filter) return node:walk(filter) end +--- Rewrites a Meta scalar or list value so every entry becomes a `RawInline` +-- in the given format, verbatim -- bypassing that format's writer-side string +-- escaping entirely. Preserves the scalar-vs-list shape of the input. +-- Useful for metadata values (paths, etc.) that a pandoc writer's own +-- escaping would otherwise corrupt. `format` defaults to the current output +-- format (pandoc's `FORMAT` global) when omitted. +local function as_raw_metadata(value, format) + format = format or FORMAT + local function raw(str) + return pandoc.MetaInlines({ pandoc.RawInline(format, str) }) + end + if get_type(value) == "List" then + local result = pandoc.List() + for _, item in ipairs(value) do + result:insert(raw(pandoc.utils.stringify(item))) + end + return result + else + return raw(pandoc.utils.stringify(value)) + end +end + return { dump = dump, type = get_type, @@ -617,6 +639,7 @@ return { }, as_inlines = as_inlines, as_blocks = as_blocks, + as_raw_metadata = as_raw_metadata, is_empty_node = is_empty_node, match = match, walk = walk, diff --git a/src/resources/pandoc/datadir/init.lua b/src/resources/pandoc/datadir/init.lua index 8eb2be0ae7..bd19df840f 100644 --- a/src/resources/pandoc/datadir/init.lua +++ b/src/resources/pandoc/datadir/init.lua @@ -971,6 +971,7 @@ quarto = { resolve_path_relative_to_document = resolvePath, as_inlines = utils.as_inlines, as_blocks = utils.as_blocks, + as_raw_metadata = utils.as_raw_metadata, is_empty_node = utils.is_empty_node, string_to_blocks = utils.string_to_blocks, string_to_inlines = utils.string_to_inlines, diff --git a/tests/docs/smoke-all/typst/typst-bibliography-leading-dot.qmd b/tests/docs/smoke-all/typst/typst-bibliography-leading-dot.qmd new file mode 100644 index 0000000000..834829c226 --- /dev/null +++ b/tests/docs/smoke-all/typst/typst-bibliography-leading-dot.qmd @@ -0,0 +1,22 @@ +--- +title: "Typst bibliography with leading-dot path" +format: + typst: + keep-typ: true +bibliography: ./refs.bib +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - ['\#bibliography\(\("\./refs\.bib"\)\)'] + - ['\\\.', '\\/'] + noErrors: default +--- + +Pandoc 3.10's Typst writer backslash-escapes any metadata string starting with a +literal dot (jgm/pandoc#11511, jgm/pandoc#11761). Quarto's `biblio.typ` partial +interpolates the raw `bibliography` metadata value into `#bibliography(...)`, so a +relative path with a leading dot (`./refs.bib`) previously compiled to invalid Typst +(`path must not contain a backslash`). This document exercises that path directly. + +See @Cronbach_1951 for a citation that must still resolve correctly. From c3d5cf9be06aa40f4e6458a152ca68f4c28ce052 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 18:41:38 +0200 Subject: [PATCH 12/17] Drop changelog entry for the leading-dot bibliography fix No released version of Quarto ever shipped Pandoc 3.10 without this fix -- it lands in the same v1.10 cycle as the version bump, so no user ever saw the broken output. Per the changelog convention, a fix for a bug introduced earlier in the same version cycle doesn't need its own entry; the eventual Pandoc/Typst dependency bump entry covers it. Same reasoning applies to the other Pandoc-3.10-driven fixes already on this branch. --- news/changelog-1.10.md | 1 - 1 file changed, 1 deletion(-) diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 05f3339520..7e945c91fb 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -38,7 +38,6 @@ All changes included in 1.10: - ([#14460](https://github.com/quarto-dev/quarto-cli/issues/14460)): Fix CSS `border` and `border-color` declarations losing tokens that precede an `rgb()`/`rgba()` color (e.g. `border: 0px solid rgb(255, 0, 0)` rendering as a 2.25pt border instead of being suppressed). Also fixes: `var(--brand-NAME)` references crashing the Typst CSS translator when `NAME` contained digits (e.g. `--brand-red-50`); a crash when an `rgba()` alpha is unparseable; the `dvmin` length unit being silently rejected (a stray space in the unit table); CSS keywords like `BOLD` not matching as `bold` (CSS keywords are case-insensitive); invalid hex colors like `#fffff` being silently accepted as 2-component colors. - ([#14511](https://github.com/quarto-dev/quarto-cli/issues/14511)): Fix brand fonts downloaded for a Typst book project not being passed to `typst compile`, causing `unknown font family` warnings and fallback to Libertinus Serif. - ([#14524](https://github.com/quarto-dev/quarto-cli/issues/14524), [quarto-ext/orange-book#4](https://github.com/quarto-ext/orange-book/pull/4)): Fix orange-book Typst book running header not honoring `lang:` — chapter heading band stayed `Chapter N.` instead of the locale's word (e.g. `Chapitre N.` for `lang: fr`). Also fixes the orange-book `list-of-figure-title` / `list-of-table-title` template pipes which were silently rendering as empty strings. Consumes the `$quarto.language.*$` template-variable namespace from [#14530](https://github.com/quarto-dev/quarto-cli/pull/14530). -- ([#14664](https://github.com/quarto-dev/quarto-cli/pull/14664)): Fix Typst render failing with `path must not contain a backslash` when `bibliography` metadata used a relative path starting with a dot (e.g. `./refs.bib` or `../refs.bib`). ### `revealjs` From fa64a90d8c6ec81e6d8194e52b30cb27348b3246 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 15 Jul 2026 18:46:08 +0200 Subject: [PATCH 13/17] Add Dependencies changelog entries for the pandoc 3.10 bump Covers pandoc, typst, typst-gather, dart-sass, esbuild, and veraPDF, all bumped together in this cycle's configuration file update. --- news/changelog-1.10.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 7e945c91fb..b0d545dfd9 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -11,6 +11,12 @@ All changes included in 1.10: ## Dependencies - ([#14291](https://github.com/quarto-dev/quarto-cli/issues/14291)): Update `deno` to v2.7.14 (fixes silent crash on Windows builds older than 16299). +- Update `pandoc` to 3.10. +- Update `typst` to 0.15.0. +- Update `typst-gather` to 0.2.3. +- Update `dart-sass` to 1.101.0. +- Update `esbuild` to 0.28.1. +- Update `veraPDF` to 1.30.2. ## Accessibility From 36d9f32b68ab275e1262d2220bd1d6e08bd9271b Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 16 Jul 2026 11:53:19 +0200 Subject: [PATCH 14/17] Fix Lua 200-local-variable overflow in the packaged filter bundle The distributable build concatenates every import()ed filter file into one physical main.lua with no per-file scoping, so all their top-level locals share Lua's hard 200-local-per-function limit. That budget was already at 199/200 before this branch; the Windows path-normalization fix added four call sites each declaring their own `local path = require("modules/path")`, tipping it to 203 and breaking the packaged-bundle smoke test at compile time. Register `path` in the existing `_quarto.modules` registry instead (costs zero locals, it's one field in an already-existing global table constructor) and have the four call sites reference `_quarto.modules.path` directly rather than each re-requiring the module as its own local. Restores the packaged bundle to 199 locals, confirmed by rebuilding the inlined bundle and compiling it with pandoc. Documented the underlying mechanism, why it keeps recurring, and the ranked fix options in llm-docs/lua-filter-bundle-local-limit.md. --- llm-docs/lua-filter-bundle-local-limit.md | 141 ++++++++++++++++++ src/resources/filters/modules/import_all.lua | 1 + .../filters/quarto-post/typst-brand-yaml.lua | 8 +- src/resources/filters/quarto-post/typst.lua | 5 +- .../filters/quarto-pre/book-links.lua | 6 +- .../filters/quarto-pre/project-paths.lua | 3 +- 6 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 llm-docs/lua-filter-bundle-local-limit.md diff --git a/llm-docs/lua-filter-bundle-local-limit.md b/llm-docs/lua-filter-bundle-local-limit.md new file mode 100644 index 0000000000..9018edf24d --- /dev/null +++ b/llm-docs/lua-filter-bundle-local-limit.md @@ -0,0 +1,141 @@ +# Lua Filter Bundling and the 200-Local-Variable Limit + +## The mechanism + +`main.lua` (`src/resources/filters/main.lua`) loads almost every filter file via +`import()`, a thin wrapper around `dofile()`: + +```lua +function import(script) + local path = PANDOC_SCRIPT_FILE:match("(.*[/\\])") + dofile(path .. script) +end +``` + +In **dev mode**, each `import()`ed file is its own `dofile()` call — its own +independent Lua chunk, with its own local-variable scope. Nothing outside that +file can see its top-level `local`s; cross-file communication only works +through globals (`quarto`, `_quarto`) or `require()`'d modules. + +For **distribution**, `package/src/common/package-filters.ts`'s `buildFilter()` +takes a different path: it parses the `-- [import] ... -- [/import]` block at +the top of `main.lua`, and **textually concatenates** every listed file's +source into one physical `main.lua`, verbatim, in order. This is a pure +string-join — it does not wrap each file's content in any scope. As a result: + +- Every top-level `local` declared in **every** inlined file becomes part of + **one single Lua chunk** (the "main function"). +- Lua enforces a hard limit of 200 local variables per function + (`MAXVARS` in Lua's compiler). Once the sum of all top-level locals across + every inlined file exceeds 200, the packaged `main.lua` fails to even + *compile*: `too many local variables (limit is 200) in main function`. +- Files loaded via `require()` (mostly things under `modules/`) are **not** + affected — `require()` resolves at runtime to its own independent chunk, + same in dev mode and in the packaged build. + +This failure only surfaces in the **"Bundle Test" / `test-bundle` CI job**, +which builds and smoke-tests the packaged distribution. Every other CI job +runs dev-mode `quarto` directly against the source tree, where each file is +still its own `dofile()` chunk — so this class of bug is invisible until the +packaged-bundle build runs. + +## Confirmed empirically (2026-07-16) + +Using a standalone reimplementation of `buildFilter()` plus pandoc itself as +a compile oracle (`pandoc doc.md --lua-filter=inlined.lua`), we measured the +actual margin on `chore/pandoc-3.10`: + +- At commit `e8921e613` (last known-good state): **199** top-level locals in + the inlined chunk — one spare slot. +- Commit `08deb91d7` added four new call sites each declaring their own + `local path = require("modules/path")` in four different `import()`ed + files (`quarto-pre/book-links.lua`, `quarto-pre/project-paths.lua`, + `quarto-post/typst-brand-yaml.lua`, `quarto-post/typst.lua`) — pushing the + count to **203** and breaking the compile. + +The codebase was already sitting at the edge (199/200) before this PR. This +is not a pandoc-3.10-specific problem — any future PR adding even one new +top-level local to an inlined file can trip it again. + +### This has already recurred multiple times + +- `c4d2198ad` (2023, PR #5146) — fixed by deleting one unused top-level local + in `crossref/listings.lua`. +- `ae392ad23` (2026-03, open PR #14170) — fixed by moving typst annotation + helpers out of an inlined file into a new `require()`-based + `modules/typst-code-annotations.lua`. +- `514baffae` (2026-04, open PR #14312) — same pattern: moved + `common/theorems.lua` logic into `modules/theorems.lua` behind `require()`. + +Every prior fix has been an ad-hoc, per-offending-file stopgap. None +addressed the inliner itself. The problem has recurred at least three times +independently within a few weeks on unrelated branches — evidence the +stopgap approach doesn't scale as filter code accumulates. + +## Fix patterns, in order of preference + +1. **Use the existing `_quarto.modules` registry instead of a fresh + `require()` alias.** `modules/import_all.lua` already centralizes most + `modules/*` requires into one table (`_quarto.modules.constants`, + `_quarto.modules.patterns`, `_quarto.modules.path`, etc.), assigned via a + single **global** table constructor — which costs **zero** additional + top-level locals no matter how many modules are registered in it. Any + `import()`ed file that needs a module already in that registry should + reference `_quarto.modules..(...)` directly instead of adding + its own `local x = require("modules/")`. This is the cheapest fix + and requires no new file. + + Caution: many pre-existing call sites (`constants` alone is re-required + as a fresh top-level local in ~10 different inlined files) do **not** + follow this convention yet, despite the module already being registered. + This is a large, low-risk cleanup opportunity — see "Known follow-up + work" below. + +2. **Move the logic into a `modules/*.lua` file loaded via `require()`.** + This is what `ae392ad23` and `514baffae` did. It fully escapes the shared + chunk (its own independent scope, same in dev and packaged builds) but + costs a new file and a new `import_all.lua` (or ad-hoc `require()`) + registration. + +3. **(Not yet implemented) Wrap each inlined file's body in `do ... end` + inside `buildFilter()`.** Verified empirically (2026-07-16, against Lua's + actual reference-implementation source in `lparser.c`) that this is + mechanically sound: Lua's compiler tracks *currently active* locals via + `reglevel`, not a cumulative count, and `do...end` blocks trigger + `leaveblock()` → `removevars()`, which rolls the active-local counter back + to what it was at block entry. So each file's top-level locals go out of + scope at that file's own `end`, freeing register slots for the next + file — the 200-slot budget only needs to cover the single most + local-heavy file, not the sum across all ~150 inlined files. + + This is the pattern real Lua bundlers (`amalg.lua`, `luabundle`) use — + though they wrap in a full closure (`function() ... end`) rather than a + plain block, because they additionally need `require()`/lazy-load/ + module-return semantics. Quarto's inliner doesn't need those semantics + (it's linear, order-preserving script concatenation, not a module + system), so a plain `do...end` wrapper is the minimal correct fix. + + Verified safe by construction: since dev-mode `dofile()` already + isolates each file into its own chunk, no filter file can rely today on + reading a *preceding* file's top-level local (that pattern would already + be broken in dev mode). Wrapping each inlined segment in `do...end` for + the packaged build only makes its scoping match dev-mode's existing + isolation — it cannot introduce new incompatibilities, though it was + only validated by compiling the wrapped bundle, not by running a full + smoke suite against it. + +## Known follow-up work (tracked separately, not urgent) + +- Sweep the ~25+ pre-existing inlined call sites that redundantly + `require()` a module already available via `_quarto.modules` (`constants` + ×~10, `patterns` ×~6, `lightbox` ×2, others) to use the registry instead. + Reclaims significant headroom; low risk, mechanical. +- Implement the `do...end` wrap in `buildFilter()` (fix pattern 3 above) as + a permanent structural fix, with a full smoke-all run against the + *packaged* bundle (not just a compile check) before merging. +- Given how often this recurs, once the process is well understood a + dedicated skill or lint check (e.g. a CI step that runs the real + `buildFilter()` + a pandoc compile-only check on every PR, rather than + discovering the break only in the full packaged smoke-test job) would + catch this class of regression immediately instead of ~20+ minutes into a + CI run. diff --git a/src/resources/filters/modules/import_all.lua b/src/resources/filters/modules/import_all.lua index 61429636fc..b642911129 100644 --- a/src/resources/filters/modules/import_all.lua +++ b/src/resources/filters/modules/import_all.lua @@ -16,6 +16,7 @@ _quarto.modules = { lightbox = require("modules/lightbox"), mediabag = require("modules/mediabag"), openxml = require("modules/openxml"), + path = require("modules/path"), patterns = require("modules/patterns"), scope = require("modules/scope"), string = require("modules/string"), diff --git a/src/resources/filters/quarto-post/typst-brand-yaml.lua b/src/resources/filters/quarto-post/typst-brand-yaml.lua index d530d8c75e..ae2243cf00 100644 --- a/src/resources/filters/quarto-post/typst-brand-yaml.lua +++ b/src/resources/filters/quarto-post/typst-brand-yaml.lua @@ -1,5 +1,3 @@ -local path = require 'modules/path' - function render_typst_brand_yaml() if not _quarto.format.isTypstOutput() then return {} @@ -113,7 +111,7 @@ function render_typst_brand_yaml() for name, image in pairs(brandLogo.images) do declImage[name] = { -- Typst 0.15+ rejects backslash path separators. - path = path.to_forward_slashes(quote_string(image.path)), + path = _quarto.modules.path.to_forward_slashes(quote_string(image.path)), alt = quote_string(image.alt), } end @@ -125,7 +123,7 @@ function render_typst_brand_yaml() if brandLogo[size] then declLogo[size] = { -- Typst 0.15+ rejects backslash path separators. - path = path.to_forward_slashes(quote_string(brandLogo[size].path)), + path = _quarto.modules.path.to_forward_slashes(quote_string(brandLogo[size].path)), alt = quote_string(brandLogo[size].alt), } end @@ -333,7 +331,7 @@ function render_typst_brand_yaml() imageFilename = imageFilename and imageFilename:gsub('\\_', '_') else -- Typst 0.15+ rejects backslash path separators. - imageFilename = path.to_forward_slashes(imageFilename) + imageFilename = _quarto.modules.path.to_forward_slashes(imageFilename) end logoOptions.path = pandoc.RawInline('typst', imageFilename) meta.logo = logoOptions diff --git a/src/resources/filters/quarto-post/typst.lua b/src/resources/filters/quarto-post/typst.lua index d89a31eea8..c4a7bfe19a 100644 --- a/src/resources/filters/quarto-post/typst.lua +++ b/src/resources/filters/quarto-post/typst.lua @@ -8,7 +8,6 @@ local typst = require("modules/typst") _quarto.format.typst = typst -local path = require("modules/path") -- Helper to format marginalia shift parameter -- auto/true/false are unquoted, "avoid"/"ignore" are quoted strings @@ -254,7 +253,7 @@ function render_typst_fixups() local params = {} -- Typst 0.15+ rejects backslash path separators in image() calls. - src = path.to_forward_slashes(src) + src = _quarto.modules.path.to_forward_slashes(src) table.insert(params, '"' .. src .. '"') -- Alt text second (escape backslashes and quotes) @@ -288,7 +287,7 @@ function render_typst_fixups() -- Typst 0.15+ rejects backslash path separators in image() calls, and -- this bare-Image path (no alt text) is handed to Pandoc's own Typst -- writer, which emits image.src verbatim. - image.src = path.to_forward_slashes(image.src) + image.src = _quarto.modules.path.to_forward_slashes(image.src) return image end, diff --git a/src/resources/filters/quarto-pre/book-links.lua b/src/resources/filters/quarto-pre/book-links.lua index b080b88e2f..f277e24c84 100644 --- a/src/resources/filters/quarto-pre/book-links.lua +++ b/src/resources/filters/quarto-pre/book-links.lua @@ -1,9 +1,7 @@ -- book-links.lua -- Copyright (C) 2020-2022 Posit Software, PBC -local path = require 'modules/path' - -function index_book_file_targets() +function index_book_file_targets() if not param("single-file-book", false) then return {} else @@ -55,7 +53,7 @@ function resolve_book_file_targets() package.config:sub(1,1) -- Paths are always using '/' separator (even on windows) - linkTarget = path.to_forward_slashes(linkTarget) + linkTarget = _quarto.modules.path.to_forward_slashes(linkTarget) local sectionId = quarto_global_state.fileSectionIds[linkTarget]; if sectionId ~= nil then el.target = '#' .. sectionId diff --git a/src/resources/filters/quarto-pre/project-paths.lua b/src/resources/filters/quarto-pre/project-paths.lua index c3fb306ba7..af1b1117db 100644 --- a/src/resources/filters/quarto-pre/project-paths.lua +++ b/src/resources/filters/quarto-pre/project-paths.lua @@ -2,14 +2,13 @@ -- Copyright (C) 2020-2022 Posit Software, PBC local constants = require("modules/constants") -local pathmodule = require("modules/path") local function resolveProjectPath(path) local offset = _quarto.projectOffset() if offset and path and startsWith(path, '/') then -- This path can flow into writers (e.g. Typst 0.15+) that reject -- backslash path separators, so it needs forward slashes. - return pathmodule.to_forward_slashes(pandoc.path.join({offset, pandoc.text.sub(path, 2, #path)})) + return _quarto.modules.path.to_forward_slashes(pandoc.path.join({offset, pandoc.text.sub(path, 2, #path)})) else return nil end From 17422c580376beb49efbd517c40df9777ae883fd Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 16 Jul 2026 12:00:29 +0200 Subject: [PATCH 15/17] Document the _quarto.modules registry convention for import()ed Lua files Files loaded via import() get textually concatenated into one shared chunk for the packaged distribution build, so a fresh top-level require() alias in one of them competes for Lua's 200-local limit across every other inlined file. Point at the existing _quarto.modules registry instead, which costs zero extra locals. --- .claude/rules/filters/lua-development.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.claude/rules/filters/lua-development.md b/.claude/rules/filters/lua-development.md index b9da063f62..42db96f78b 100644 --- a/.claude/rules/filters/lua-development.md +++ b/.claude/rules/filters/lua-development.md @@ -42,6 +42,20 @@ local md = require("modules/md") import("./modules/patterns.lua") ``` +### In `import()`ed Files, Prefer the `_quarto.modules` Registry Over a Fresh `require()` + +Files loaded via `import()` are textually concatenated into one shared Lua chunk for +the packaged distribution build, so every top-level `local` they declare — including +`local x = require("modules/y")` — competes for Lua's hard 200-local-per-function +limit across *all* inlined files combined. Most `modules/*` are already registered +in `_quarto.modules` by `modules/import_all.lua` (a single global table assignment, +so it costs zero additional locals). In an `import()`ed file, reference +`_quarto.modules..(...)` directly instead of adding a new top-level +`local = require("modules/")`. This doesn't apply inside files loaded +via `require()` themselves — those get their own independent chunk and aren't +affected. See [llm-docs/lua-filter-bundle-local-limit.md](../../../llm-docs/lua-filter-bundle-local-limit.md) +for the full mechanism and why this keeps recurring. + ## Custom Node Patterns ### Walking Custom Nodes From d146b628eacc7b09ad01c59fd1b46a2941c3c039 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 16 Jul 2026 15:30:30 +0200 Subject: [PATCH 16/17] Fix feature-format-matrix typst regex drift from pandoc 3.10 Pandoc 3.10 always emits a newline after `#set text(...)` directives in the Typst writer (jgm/pandoc#11583), fixing a real bug where a lang- or style-attributed Div whose first block was a list would be mis-parsed by Typst (list markers must start on a new line). The fix applies unconditionally, so it also touches the simple inline cases these tests assert on, even though the newline is a no-op there (Typst collapses it to whitespace in markup mode). Loosened each affected regex to tolerate the whitespace change rather than requiring an exact single space, so it also matches if pandoc's formatting shifts again. The raw-blocks/pandoc-json dash-escaping case is a related but distinct drift: consecutive hyphens are now escaped individually; verified via pdftotext that the compiled PDF still renders literal "--", not an en dash. --- .../qmd-files/css-properties/color/color-fuchsia.qmd | 2 +- .../qmd-files/css-properties/color/color-magenta.qmd | 2 +- .../qmd-files/css-properties/color/color-mediumseagreen.qmd | 2 +- .../qmd-files/css-properties/color/color-rgb-hex.qmd | 2 +- .../qmd-files/css-properties/color/color-transparent.qmd | 2 +- .../qmd-files/css-properties/color/document.qmd | 2 +- .../qmd-files/css-properties/font-family/document.qmd | 2 +- .../qmd-files/css-properties/font-size/cell-percentage.qmd | 2 +- .../qmd-files/css-properties/font-size/document.qmd | 2 +- .../css-properties/font-style/div-font-style-italic.qmd | 2 +- .../css-properties/font-style/td-font-style-italic.qmd | 2 +- .../css-properties/font-style/td-font-style-normal.qmd | 2 +- .../css-properties/font-style/td-font-style-oblique.qmd | 2 +- .../css-properties/font-weight/div-font-weight-demi-bold.qmd | 2 +- .../css-properties/font-weight/td-font-weight-bold.qmd | 2 +- .../css-properties/font-weight/td-font-weight-ultra-light.qmd | 2 +- .../qmd-files/css-properties/opacity/document.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-mediumseagreen.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-nocolor.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-one.qmd | 2 +- .../css-properties/opacity/opacity-rgb-comma-alpha-fraction.qmd | 2 +- .../css-properties/opacity/opacity-rgb-comma-alpha-int.qmd | 2 +- .../css-properties/opacity/opacity-rgb-comma-alpha-percent.qmd | 2 +- .../css-properties/opacity/opacity-rgb-comma-percent-alpha.qmd | 2 +- .../css-properties/opacity/opacity-rgb-comma-percent.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-rgb-comma.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-rgb-hex-alpha.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-rgb-hex.qmd | 2 +- .../css-properties/opacity/opacity-rgb-shorthex-alpha.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-rgb-shorthex.qmd | 2 +- .../css-properties/opacity/opacity-rgb-space-alpha-fraction.qmd | 2 +- .../css-properties/opacity/opacity-rgb-space-alpha-percent.qmd | 2 +- .../opacity/opacity-rgb-space-percent-alpha-fraction.qmd | 2 +- .../opacity/opacity-rgb-space-percent-alpha-none.qmd | 2 +- .../opacity/opacity-rgb-space-percent-alpha-percent.qmd | 2 +- .../css-properties/opacity/opacity-rgb-space-percent.qmd | 2 +- .../qmd-files/css-properties/opacity/opacity-rgb-space.qmd | 2 +- .../qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-fuchsia.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-fuchsia.qmd index 99cf9d49e5..7ef7207877 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-fuchsia.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-fuchsia.qmd @@ -20,7 +20,7 @@ _quarto: ensureTypstFileRegexMatches: - # note css fuchsia #f012be is not the same as css fuchsia #ff00ff - - '\[#set text\(fill: fuchsia\); B\]' + - '\[#set text\(fill: fuchsia\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-magenta.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-magenta.qmd index 71585f9463..af4b1d908c 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-magenta.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-magenta.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(255, 0, 255\)\); B\]' + - '\[#set text\(fill: rgb\(255, 0, 255\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-mediumseagreen.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-mediumseagreen.qmd index 366d2196c1..0f5f873c98 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-mediumseagreen.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-mediumseagreen.qmd @@ -20,7 +20,7 @@ _quarto: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(60, 179, 113\)\); B\]' + - '\[#set text\(fill: rgb\(60, 179, 113\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-rgb-hex.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-rgb-hex.qmd index d254c5eec5..dc4f78fceb 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-rgb-hex.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-rgb-hex.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\("#000066"\)\); B\]' + - '\[#set text\(fill: rgb\("#000066"\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-transparent.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-transparent.qmd index 1a1e0a07cf..f9f2618457 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-transparent.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/color-transparent.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(0, 0, 0, 0%\)\); B\]' + - '\[#set text\(fill: rgb\(0, 0, 0, 0%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/document.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/document.qmd index 58aa758687..0a1eb03f30 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/color/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/color/document.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: maroon\); B\]' + - '\[#set text\(fill: maroon\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-family/document.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-family/document.qmd index c068b1c042..58df5bbae5 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-family/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-family/document.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#{set text\(font: \("Georgia", "serif"\)\); table\(' + - '#{set text\(font: \("Georgia", "serif"\)\);\s*table\(' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/cell-percentage.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/cell-percentage.qmd index 8ff3c17d2f..ad191c3fe3 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/cell-percentage.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/cell-percentage.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(size: 0.8em\); B\]' + - '\[#set text\(size: 0.8em\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/document.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/document.qmd index 6b628220b2..18be8a80c5 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-size/document.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#{set text\(size: 6pt\); table\(' + - '#{set text\(size: 6pt\);\s*table\(' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/div-font-style-italic.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/div-font-style-italic.qmd index 98d7ba9cb8..95f90ed2dc 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/div-font-style-italic.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/div-font-style-italic.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#block\[\s*#set text\(style: "italic"\); Italic div\s*\]' + - '#block\[\s*#set text\(style: "italic"\);\s*Italic div\s*\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-italic.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-italic.qmd index 4ee03425f7..412faa7b53 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-italic.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-italic.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(style: "italic"\); B\]' + - '\[#set text\(style: "italic"\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-normal.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-normal.qmd index ffb5d51964..53e8e08048 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-normal.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-normal.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(style: "normal"\); B\]' + - '\[#set text\(style: "normal"\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-oblique.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-oblique.qmd index bbdd55add3..0877b84351 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-oblique.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-style/td-font-style-oblique.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(style: "oblique"\); B\]' + - '\[#set text\(style: "oblique"\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/div-font-weight-demi-bold.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/div-font-weight-demi-bold.qmd index c566e82541..36dca59b15 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/div-font-weight-demi-bold.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/div-font-weight-demi-bold.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#block\[\s*#set text\(weight: "semibold"\); Demi-bold div\s*\]' + - '#block\[\s*#set text\(weight: "semibold"\);\s*Demi-bold div\s*\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-bold.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-bold.qmd index 71585f9463..af4b1d908c 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-bold.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-bold.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(255, 0, 255\)\); B\]' + - '\[#set text\(fill: rgb\(255, 0, 255\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-ultra-light.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-ultra-light.qmd index aa947fa4e0..9cb68594be 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-ultra-light.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/font-weight/td-font-weight-ultra-light.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(weight: "extralight"\); B\]' + - '\[#set text\(weight: "extralight"\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/document.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/document.qmd index be2140439d..54b392a4e2 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/document.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\("#85144b7f"\)\); B\]' + - '\[#set text\(fill: rgb\("#85144b7f"\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-mediumseagreen.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-mediumseagreen.qmd index 669fabf0a7..f4e2f1ade8 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-mediumseagreen.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-mediumseagreen.qmd @@ -20,7 +20,7 @@ _quarto: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(60, 179, 113, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(60, 179, 113, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-nocolor.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-nocolor.qmd index de52a2db1c..e1d832c079 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-nocolor.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-nocolor.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(0, 0, 0, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(0, 0, 0, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-one.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-one.qmd index f443d9643c..38427b9467 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-one.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-one.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(0, 0, 0, 100%\)\); B\]' + - '\[#set text\(fill: rgb\(0, 0, 0, 100%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-fraction.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-fraction.qmd index 36df23baaa..a42ee17cf4 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-fraction.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-fraction.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(70, 130, 180, 17%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 17%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-int.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-int.qmd index 3b23fb1eff..42e83d271f 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-int.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-int.qmd @@ -20,7 +20,7 @@ _quarto: ensureTypstFileRegexMatches: - # note the input is invalid, there is no "int alpha" input for rgb() in css - - '\[#set text\(fill: rgb\(70, 130, 180, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-percent.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-percent.qmd index af491fd07d..2062658b54 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-percent.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-alpha-percent.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(70, 130, 180, 30%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 30%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent-alpha.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent-alpha.qmd index 208d426507..a0b943718a 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent-alpha.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent-alpha.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(94%, 42%, 0%, 37.5%\)\); B\]' + - '\[#set text\(fill: rgb\(94%, 42%, 0%, 37.5%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent.qmd index 315c3128c3..dcd49ca1b5 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma-percent.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(90%, 85%, 30%, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(90%, 85%, 30%, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma.qmd index d85fef961f..a8dd9ee8a9 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-comma.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(70, 130, 180, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex-alpha.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex-alpha.qmd index 2491bd8e2a..e04a31c721 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex-alpha.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex-alpha.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\("#4682b440"\)\); B\]' + - '\[#set text\(fill: rgb\("#4682b440"\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex.qmd index c3b3b416c3..17821136de 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-hex.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\("#4682b47f"\)\); B\]' + - '\[#set text\(fill: rgb\("#4682b47f"\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex-alpha.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex-alpha.qmd index 3ef612801d..2f3d8e86a3 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex-alpha.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex-alpha.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\("#c1d4"\)\); B\]' + - '\[#set text\(fill: rgb\("#c1d4"\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex.qmd index e7560680d5..19d4902b3e 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-shorthex.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\("#c1d7"\)\); B\]' + - '\[#set text\(fill: rgb\("#c1d7"\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-fraction.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-fraction.qmd index b372d396bd..fa32768768 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-fraction.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-fraction.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(70, 130, 180, 45%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 45%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-percent.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-percent.qmd index 8c7bc367c8..2aa0dc7f11 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-percent.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-alpha-percent.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(70, 130, 180, 42%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 42%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-fraction.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-fraction.qmd index 0a45f594bb..82ff540b71 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-fraction.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-fraction.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(94%, 42%, 0%, 36%\)\); B\]' + - '\[#set text\(fill: rgb\(94%, 42%, 0%, 36%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-none.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-none.qmd index 04d0164793..837eb0a531 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-none.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-none.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(94%, 42%, 0%, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(94%, 42%, 0%, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-percent.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-percent.qmd index 2da64581d6..a3f18770b4 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-percent.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent-alpha-percent.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(94%, 42%, 0%, 42%\)\); B\]' + - '\[#set text\(fill: rgb\(94%, 42%, 0%, 42%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent.qmd index 2c477f8ee2..80e1de3e07 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space-percent.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(94%, 42%, 0%, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(94%, 42%, 0%, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space.qmd b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space.qmd index 46f078b3ff..e322dd91f1 100644 --- a/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/css-properties/opacity/opacity-rgb-space.qmd @@ -19,7 +19,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '\[#set text\(fill: rgb\(70, 130, 180, 50%\)\); B\]' + - '\[#set text\(fill: rgb\(70, 130, 180, 50%\)\);\s*B\]' - [] --- diff --git a/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd b/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd index 604bbff047..b9e34b7582 100644 --- a/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd @@ -27,7 +27,7 @@ _quarto: - ["–"] typst: ensureTypstFileRegexMatches: - - ["--"] + - ['do-\\-not-\\-escape-\\-this'] # each hyphen pair is now escaped individually; still renders as literal -- (verified via pdftotext), not en dash - ["–"] latex: ensureFileRegexMatches: From b28f7bda35d1bd30d08170dc3272d4a9a0e32b04 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 16 Jul 2026 17:42:05 +0200 Subject: [PATCH 17/17] Update some gitignore modified by Quarto from running tests --- dev-docs/feature-format-matrix/.gitignore | 2 ++ tests/docs/smoke-all/typst/.gitignore | 3 +++ .../typst/brand-yaml/typography/brand-extension/.gitignore | 2 ++ tests/docs/smoke-all/typst/no-packages-project/.gitignore | 2 ++ tests/docs/smoke-all/typst/relative-font-path/.gitignore | 2 ++ 5 files changed, 11 insertions(+) create mode 100644 tests/docs/smoke-all/typst/brand-yaml/typography/brand-extension/.gitignore create mode 100644 tests/docs/smoke-all/typst/no-packages-project/.gitignore create mode 100644 tests/docs/smoke-all/typst/relative-font-path/.gitignore diff --git a/dev-docs/feature-format-matrix/.gitignore b/dev-docs/feature-format-matrix/.gitignore index 58d8cfd09b..48856b5ebc 100644 --- a/dev-docs/feature-format-matrix/.gitignore +++ b/dev-docs/feature-format-matrix/.gitignore @@ -7,3 +7,5 @@ qmd-files/**/*.md qmd-files/**/*.mdx /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/typst/.gitignore b/tests/docs/smoke-all/typst/.gitignore index 3e0982561e..25197bc23c 100644 --- a/tests/docs/smoke-all/typst/.gitignore +++ b/tests/docs/smoke-all/typst/.gitignore @@ -1,3 +1,6 @@ # Generated test outputs (from keep-typ option) *.typ *.pdf + +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/typst/brand-yaml/typography/brand-extension/.gitignore b/tests/docs/smoke-all/typst/brand-yaml/typography/brand-extension/.gitignore new file mode 100644 index 0000000000..ad293093b0 --- /dev/null +++ b/tests/docs/smoke-all/typst/brand-yaml/typography/brand-extension/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/typst/no-packages-project/.gitignore b/tests/docs/smoke-all/typst/no-packages-project/.gitignore new file mode 100644 index 0000000000..ad293093b0 --- /dev/null +++ b/tests/docs/smoke-all/typst/no-packages-project/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/typst/relative-font-path/.gitignore b/tests/docs/smoke-all/typst/relative-font-path/.gitignore new file mode 100644 index 0000000000..ad293093b0 --- /dev/null +++ b/tests/docs/smoke-all/typst/relative-font-path/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb