From ff7843d3ae5db19aa5bef5c0ee8e467e9fac77a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:49:59 +0100 Subject: [PATCH 1/5] fix: neutralise inline code in title scaffolds for Typst output Quarto wraps theorem/example titles in __quarto_custom_scaffold Divs with inline-only content. Both the code-window box wrapping and Pandoc's Skylighting generate Typst function calls (#box(...), #NormalTok(...)) that get stringified as literal text when Quarto renders the title parameter as a string. Replace the global Code filter with a selective document walk that converts Code to plain Typst backtick code inside title scaffolds and applies the full box styling everywhere else. --- CHANGELOG.md | 1 + .../_modules/hotfix/skylighting-typst-fix.lua | 62 +++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d903ca..f0e3fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug Fixes - fix: unwrap Quarto's `DecoratedCodeBlock` Div to prevent double filename wrapping in Typst output. +- fix: prevent inline code in theorem/example titles from being converted to raw Typst markup that gets stringified. - fix: normalise code blocks with no or unknown language class to `default` for consistent styling across all formats. - fix: default to `#` comment symbol for unknown code block languages (`default`, `txt`, etc.) in annotation detection. - fix: support code annotations with `syntax-highlighting: idiomatic` (native Typst highlighting) via a `show raw.line` rule. diff --git a/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua b/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua index b63fb13..251766e 100644 --- a/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua +++ b/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua @@ -218,18 +218,70 @@ function Pandoc(doc) return doc end ---- Wrap inline Code elements with a background box in Typst output. -function Code(el) +--- Check if a Div is a Quarto title scaffold (inline-only content). +--- Quarto wraps theorem/example titles in __quarto_custom_scaffold Divs +--- containing a single Plain or Para block. Converting Code to RawInline +--- inside these Divs causes the raw Typst markup to be stringified as +--- literal text in the title parameter. +--- @param div pandoc.Div +--- @return boolean +local function is_title_scaffold(div) + if div.attributes['__quarto_custom_scaffold'] ~= 'true' then + return false + end + for _, child in ipairs(div.content) do + if child.t ~= 'Plain' and child.t ~= 'Para' then + return false + end + end + return true +end + +--- Convert Code to plain Typst backtick code inside title scaffolds. +--- Prevents both the code-window box wrapping AND Pandoc's Skylighting +--- from generating Typst function calls that get stringified by Quarto. +--- @param el pandoc.Code +--- @return pandoc.RawInline +local function neutralise_title_code(el) + return pandoc.RawInline('typst', '`' .. el.text .. '`') +end + +--- Walk the document tree and convert inline Code to RawInline with +--- background styling, skipping title scaffolds where the conversion +--- would produce raw Typst markup that gets stringified by Quarto. +local function process_inline_code(doc) if not quarto.doc.is_format('typst') then - return el + return doc end - return process_typst_inline(el) + + local code_filter = { Code = function(el) return process_typst_inline(el) end } + local title_filter = { Code = neutralise_title_code } + + local function walk_blocks(blocks) + local new_blocks = {} + for _, blk in ipairs(blocks) do + if blk.t == 'Div' then + if is_title_scaffold(blk) then + table.insert(new_blocks, blk:walk(title_filter)) + else + blk.content = walk_blocks(blk.content) + table.insert(new_blocks, blk) + end + else + table.insert(new_blocks, blk:walk(code_filter)) + end + end + return pandoc.Blocks(new_blocks) + end + + doc.blocks = walk_blocks(doc.blocks) + return doc end return { set_wrapper = set_wrapper, filters = { { Pandoc = Pandoc }, - { Code = Code }, + { Pandoc = process_inline_code }, }, } From e0b0f0c24752f54afe5258fcb42704e7a3719e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:21:44 +0100 Subject: [PATCH 2/5] fix: evaluate theorem title strings as Typst markup Quarto renders custom type titles as title: "..." (string mode) which stringifies any Typst markup. Inline code in titles produces Skylighting tokens with inner quotes that break the Typst string syntax. Two-part fix: - Pre-quarto: convert Code in title scaffolds to plain Typst backtick code, avoiding Skylighting tokens that contain unescaped quotes. - Post-quarto: inject a Typst override of simple-theorem-render that evaluates string titles with eval(mode: "markup"), giving title: [...] semantics even though Quarto emits title: "...". --- CHANGELOG.md | 2 +- _extensions/code-window/_extension.yml | 1 + .../_modules/hotfix/skylighting-typst-fix.lua | 26 +++--- .../_modules/hotfix/typst-title-fix.lua | 79 +++++++++++++++++++ 4 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 _extensions/code-window/_modules/hotfix/typst-title-fix.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e3fd6..145c624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Bug Fixes - fix: unwrap Quarto's `DecoratedCodeBlock` Div to prevent double filename wrapping in Typst output. -- fix: prevent inline code in theorem/example titles from being converted to raw Typst markup that gets stringified. +- fix: evaluate theorem/example title strings as Typst markup so inline code renders correctly instead of being stringified. - fix: normalise code blocks with no or unknown language class to `default` for consistent styling across all formats. - fix: default to `#` comment symbol for unknown code block languages (`default`, `txt`, etc.) in annotation detection. - fix: support code annotations with `syntax-highlighting: idiomatic` (native Typst highlighting) via a `show raw.line` rule. diff --git a/_extensions/code-window/_extension.yml b/_extensions/code-window/_extension.yml index 8badc0f..82af68a 100644 --- a/_extensions/code-window/_extension.yml +++ b/_extensions/code-window/_extension.yml @@ -6,3 +6,4 @@ contributes: filters: - at: pre-quarto path: main.lua + - path: _modules/hotfix/typst-title-fix.lua diff --git a/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua b/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua index 251766e..a409963 100644 --- a/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua +++ b/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua @@ -219,10 +219,6 @@ function Pandoc(doc) end --- Check if a Div is a Quarto title scaffold (inline-only content). ---- Quarto wraps theorem/example titles in __quarto_custom_scaffold Divs ---- containing a single Plain or Para block. Converting Code to RawInline ---- inside these Divs causes the raw Typst markup to be stringified as ---- literal text in the title parameter. --- @param div pandoc.Div --- @return boolean local function is_title_scaffold(div) @@ -237,25 +233,23 @@ local function is_title_scaffold(div) return true end ---- Convert Code to plain Typst backtick code inside title scaffolds. ---- Prevents both the code-window box wrapping AND Pandoc's Skylighting ---- from generating Typst function calls that get stringified by Quarto. ---- @param el pandoc.Code ---- @return pandoc.RawInline -local function neutralise_title_code(el) - return pandoc.RawInline('typst', '`' .. el.text .. '`') -end - --- Walk the document tree and convert inline Code to RawInline with ---- background styling, skipping title scaffolds where the conversion ---- would produce raw Typst markup that gets stringified by Quarto. +--- background styling. Code in title scaffolds is converted to plain +--- Typst backtick code to avoid Skylighting tokens with inner quotes +--- that would break the string parameter Quarto generates. +--- The typst-title-fix post-quarto filter then evaluates the string +--- as markup so the backtick code renders with proper inline styling. local function process_inline_code(doc) if not quarto.doc.is_format('typst') then return doc end local code_filter = { Code = function(el) return process_typst_inline(el) end } - local title_filter = { Code = neutralise_title_code } + local title_filter = { + Code = function(el) + return pandoc.RawInline('typst', '`' .. el.text .. '`') + end, + } local function walk_blocks(blocks) local new_blocks = {} diff --git a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua new file mode 100644 index 0000000..2da5040 --- /dev/null +++ b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua @@ -0,0 +1,79 @@ +--- @module typst-title-fix +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Hot-fix for Quarto rendering theorem titles as string parameters. +--- Quarto renders custom type titles as title: "..." (string mode) which +--- stringifies any Typst markup. This filter injects a Typst show rule +--- that evaluates string titles as markup so inline code and other +--- formatting render correctly in theorem/example titles. + +--- Typst code that overrides simple-theorem-render to evaluate string +--- titles as Typst markup. Injected after the template definitions so +--- the override replaces the default. The make-frame calls are then +--- re-created with the fixed render function. +local TYPST_TITLE_FIX = [==[ +// code-window: hot-fix for Quarto rendering theorem titles as strings. +// Redefine simple-theorem-render to evaluate string titles as Typst markup. +// This produces title: [...] semantics even though Quarto emits title: "...". +#let simple-theorem-render(prefix: none, title: "", full-title: auto, body) = { + if full-title != "" and full-title != auto and full-title != none { + let rendered-title = if type(full-title) == str { + eval(full-title, mode: "markup") + } else { + full-title + } + strong[#rendered-title.] + h(0.5em) + } + emph(body) + parbreak() +} +]==] + +return { + { + Pandoc = function(doc) + if not quarto.doc.is_format('typst') then + return doc + end + + -- Only inject if there are __quarto_custom Theorem Divs in the document. + local has_theorems = false + local function check_blocks(blocks) + for _, blk in ipairs(blocks) do + if blk.t == 'Div' and blk.attributes['__quarto_custom_type'] == 'Theorem' then + has_theorems = true + return + end + if blk.t == 'Div' then + check_blocks(blk.content) + end + end + end + check_blocks(doc.blocks) + if not has_theorems then + return doc + end + + -- Guard: skip if already injected. + for _, blk in ipairs(doc.blocks) do + if blk.t == 'RawBlock' and blk.format == 'typst' + and blk.text:find('code-window: hot-fix for Quarto rendering theorem titles', 1, true) then + return doc + end + end + + -- Insert at the end of the preamble (before the first non-RawBlock). + local insert_pos = 1 + for idx, blk in ipairs(doc.blocks) do + if blk.t ~= 'RawBlock' then + insert_pos = idx + break + end + end + table.insert(doc.blocks, insert_pos, pandoc.RawBlock('typst', TYPST_TITLE_FIX)) + return doc + end, + }, +} From 3321bcd8ca1f6fae31c7bc23fdeee93813c6a528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:30:21 +0100 Subject: [PATCH 3/5] fix: wrap theorem functions to evaluate string titles as markup The previous approach redefined simple-theorem-render, but make-frame captures the render function by value at definition time, so the override had no effect. Instead, scan the Typst preamble for make-frame definitions, extract the generated function names (e.g., example, theorem), and inject wrapper functions that evaluate string title parameters with eval(mode: "markup") before delegating to the original function. --- _extensions/code-window/_extension.yml | 3 +- .../_modules/hotfix/typst-title-fix.lua | 70 ++++++++++++------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/_extensions/code-window/_extension.yml b/_extensions/code-window/_extension.yml index 82af68a..36c17df 100644 --- a/_extensions/code-window/_extension.yml +++ b/_extensions/code-window/_extension.yml @@ -6,4 +6,5 @@ contributes: filters: - at: pre-quarto path: main.lua - - path: _modules/hotfix/typst-title-fix.lua + - at: post-quarto + path: _modules/hotfix/typst-title-fix.lua diff --git a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua index 2da5040..e7e0b5c 100644 --- a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua +++ b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua @@ -4,33 +4,34 @@ --- @author Mickaël Canouil --- @brief Hot-fix for Quarto rendering theorem titles as string parameters. --- Quarto renders custom type titles as title: "..." (string mode) which ---- stringifies any Typst markup. This filter injects a Typst show rule ---- that evaluates string titles as markup so inline code and other ---- formatting render correctly in theorem/example titles. +--- stringifies any Typst markup. This post-quarto filter scans the Typst +--- preamble for make-frame definitions, then injects wrapper functions that +--- evaluate string titles as Typst markup via eval(mode: "markup"). ---- Typst code that overrides simple-theorem-render to evaluate string ---- titles as Typst markup. Injected after the template definitions so ---- the override replaces the default. The make-frame calls are then ---- re-created with the fixed render function. -local TYPST_TITLE_FIX = [==[ -// code-window: hot-fix for Quarto rendering theorem titles as strings. -// Redefine simple-theorem-render to evaluate string titles as Typst markup. -// This produces title: [...] semantics even though Quarto emits title: "...". -#let simple-theorem-render(prefix: none, title: "", full-title: auto, body) = { - if full-title != "" and full-title != auto and full-title != none { - let rendered-title = if type(full-title) == str { - eval(full-title, mode: "markup") - } else { - full-title - } - strong[#rendered-title.] - h(0.5em) +--- Typst wrapper template. %s is replaced with the function name. +local WRAPPER_TEMPLATE = [==[ +#let _cw-orig-%s = %s +#let %s(title: none, ..args) = { + let t = if title != none and type(title) == str { + eval(title, mode: "markup") + } else { + title } - emph(body) - parbreak() + _cw-orig-%s(title: t, ..args) } ]==] +--- Build Typst code that wraps each theorem function to eval string titles. +--- @param func_names table List of function names to wrap +--- @return string Typst code +local function build_wrappers(func_names) + local parts = { '// code-window: hot-fix for Quarto rendering theorem titles as strings.' } + for _, name in ipairs(func_names) do + table.insert(parts, string.format(WRAPPER_TEMPLATE, name, name, name, name)) + end + return table.concat(parts, '\n') +end + return { { Pandoc = function(doc) @@ -64,15 +65,30 @@ return { end end - -- Insert at the end of the preamble (before the first non-RawBlock). + -- Scan preamble RawBlocks for make-frame definitions to find function names. + -- Pattern: #let (xxx-counter, xxx-box, xxx, show-xxx) = make-frame( + local func_names = {} + for _, blk in ipairs(doc.blocks) do + if blk.t == 'RawBlock' and blk.format == 'typst' then + for name in blk.text:gmatch('#let %([%w%-]+%-counter, [%w%-]+%-box, ([%w%-]+), show%-[%w%-]+%) = make%-frame%(') do + table.insert(func_names, name) + end + end + end + + if #func_names == 0 then + return doc + end + + -- Insert wrappers after the last make-frame definition. local insert_pos = 1 for idx, blk in ipairs(doc.blocks) do - if blk.t ~= 'RawBlock' then - insert_pos = idx - break + if blk.t == 'RawBlock' and blk.format == 'typst' + and blk.text:find('make%-frame%(') then + insert_pos = idx + 1 end end - table.insert(doc.blocks, insert_pos, pandoc.RawBlock('typst', TYPST_TITLE_FIX)) + table.insert(doc.blocks, insert_pos, pandoc.RawBlock('typst', build_wrappers(func_names))) return doc end, }, From 6275453551d77442ccb8bbc586e74ce50108bec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:37:05 +0100 Subject: [PATCH 4/5] fix: inject theorem wrappers via doc.blocks instead of header-includes header-includes is placed before template definitions in the Typst preamble, so the theorem functions are not yet defined. RawBlocks inserted into doc.blocks appear after the template preamble, where make-frame has already created the theorem functions. Also scan the source file for cross-reference div IDs (e.g., #exm-, #thm-) to determine which theorem functions to wrap, since this information is not available in the AST at filter time. --- .../_modules/hotfix/typst-title-fix.lua | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua index e7e0b5c..02d67ef 100644 --- a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua +++ b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua @@ -4,10 +4,23 @@ --- @author Mickaël Canouil --- @brief Hot-fix for Quarto rendering theorem titles as string parameters. --- Quarto renders custom type titles as title: "..." (string mode) which ---- stringifies any Typst markup. This post-quarto filter scans the Typst ---- preamble for make-frame definitions, then injects wrapper functions that +--- stringifies any Typst markup. This post-quarto filter scans the source +--- for cross-reference div IDs, then injects Typst wrapper functions that --- evaluate string titles as Typst markup via eval(mode: "markup"). +--- Mapping from Quarto cross-reference prefix to Typst function name. +local PREFIX_TO_FUNC = { + thm = 'theorem', + lem = 'lemma', + cor = 'corollary', + prp = 'proposition', + cnj = 'conjecture', + def = 'definition', + exm = 'example', + exr = 'exercise', + sol = 'solution', +} + --- Typst wrapper template. %s is replaced with the function name. local WRAPPER_TEMPLATE = [==[ #let _cw-orig-%s = %s @@ -18,8 +31,7 @@ local WRAPPER_TEMPLATE = [==[ title } _cw-orig-%s(title: t, ..args) -} -]==] +}]==] --- Build Typst code that wraps each theorem function to eval string titles. --- @param func_names table List of function names to wrap @@ -32,6 +44,28 @@ local function build_wrappers(func_names) return table.concat(parts, '\n') end +--- Scan source files for cross-reference div IDs and return the +--- corresponding Typst function names. +--- @return table List of function names +local function detect_theorem_types() + local func_names = {} + local seen = {} + for _, input_file in ipairs(PANDOC_STATE.input_files) do + local f = io.open(input_file, 'r') + if f then + local source = f:read('*a') + f:close() + for prefix in source:gmatch('::: *{#(%w+)%-') do + if PREFIX_TO_FUNC[prefix] and not seen[prefix] then + table.insert(func_names, PREFIX_TO_FUNC[prefix]) + seen[prefix] = true + end + end + end + end + return func_names +end + return { { Pandoc = function(doc) @@ -39,24 +73,6 @@ return { return doc end - -- Only inject if there are __quarto_custom Theorem Divs in the document. - local has_theorems = false - local function check_blocks(blocks) - for _, blk in ipairs(blocks) do - if blk.t == 'Div' and blk.attributes['__quarto_custom_type'] == 'Theorem' then - has_theorems = true - return - end - if blk.t == 'Div' then - check_blocks(blk.content) - end - end - end - check_blocks(doc.blocks) - if not has_theorems then - return doc - end - -- Guard: skip if already injected. for _, blk in ipairs(doc.blocks) do if blk.t == 'RawBlock' and blk.format == 'typst' @@ -65,30 +81,16 @@ return { end end - -- Scan preamble RawBlocks for make-frame definitions to find function names. - -- Pattern: #let (xxx-counter, xxx-box, xxx, show-xxx) = make-frame( - local func_names = {} - for _, blk in ipairs(doc.blocks) do - if blk.t == 'RawBlock' and blk.format == 'typst' then - for name in blk.text:gmatch('#let %([%w%-]+%-counter, [%w%-]+%-box, ([%w%-]+), show%-[%w%-]+%) = make%-frame%(') do - table.insert(func_names, name) - end - end - end - + local func_names = detect_theorem_types() if #func_names == 0 then return doc end - -- Insert wrappers after the last make-frame definition. - local insert_pos = 1 - for idx, blk in ipairs(doc.blocks) do - if blk.t == 'RawBlock' and blk.format == 'typst' - and blk.text:find('make%-frame%(') then - insert_pos = idx + 1 - end - end - table.insert(doc.blocks, insert_pos, pandoc.RawBlock('typst', build_wrappers(func_names))) + -- Insert at the start of doc.blocks. RawBlocks placed here appear + -- after the Typst template preamble (where make-frame defines the + -- theorem functions), so the wrappers can reference them. + table.insert(doc.blocks, 1, pandoc.RawBlock('typst', build_wrappers(func_names))) + return doc end, }, From 5b007b20080872e26eba989675191b9f272d5d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:00:40 +0100 Subject: [PATCH 5/5] feat: support per-hotfix quarto-version thresholds Each hotfix can now have its own quarto-version threshold for auto-disable, since upstream fixes are unlikely to land in the same Quarto release. The hotfix value can be a boolean or a map with enabled and quarto-version keys: hotfix: code-annotations: quarto-version: "1.10.0" skylighting: quarto-version: "1.11.0" typst-title: quarto-version: "1.10.0" The global quarto-version key is still supported as a fallback. Add typst-title as a new hotfix key for the theorem title fix. --- CHANGELOG.md | 4 ++ .../_modules/hotfix/typst-title-fix.lua | 9 ++++ _extensions/code-window/code-window.lua | 46 ++++++++++++++++--- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145c624..e2dec39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - fix: default to `#` comment symbol for unknown code block languages (`default`, `txt`, etc.) in annotation detection. - fix: support code annotations with `syntax-highlighting: idiomatic` (native Typst highlighting) via a `show raw.line` rule. +### New Features + +- feat: support per-hotfix `quarto-version` thresholds for independent auto-disable. + ### Refactoring - refactor: extract language normalisation into dedicated `_modules/language.lua` module. diff --git a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua index 02d67ef..7847f96 100644 --- a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua +++ b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua @@ -73,6 +73,15 @@ return { return doc end + -- Check if the hotfix is enabled via metadata set by the pre-quarto filter. + local hotfix_meta = doc.meta['_code-window-hotfix'] + if hotfix_meta then + local enabled = hotfix_meta['typst-title'] + if enabled and pandoc.utils.stringify(enabled) == 'false' then + return doc + end + end + -- Guard: skip if already injected. for _, blk in ipairs(doc.blocks) do if blk.t == 'RawBlock' and blk.format == 'typst' diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index efd0ef3..3a7f9d5 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -43,6 +43,7 @@ local DEFAULTS = { local HOTFIX_DEFAULTS = { ['code-annotations'] = true, ['skylighting'] = true, + ['typst-title'] = true, } local CURRENT_FORMAT = nil @@ -454,9 +455,13 @@ function Meta(meta) '"skylighting-fix" is deprecated. Use "hotfix: { skylighting: true/false }" instead.') end - -- Parse hotfix options with version-based auto-disable. + -- Parse hotfix options with per-hotfix version-based auto-disable. + -- Each hotfix value can be: + -- boolean/string: true/false to enable/disable + -- map: { enabled: true/false, quarto-version: "x.y.z" } + -- A global quarto-version key is also supported as a fallback. local hotfix = {} - local hotfix_version_override = false + local global_version_disabled = false if hotfix_meta then local version_str = hotfix_meta['quarto-version'] if version_str then @@ -464,17 +469,36 @@ function Meta(meta) if version_str ~= '' then local ok, threshold = pcall(pandoc.types.Version, version_str) if ok and quarto.version >= threshold then - hotfix_version_override = true + global_version_disabled = true end end end end for key, default in pairs(HOTFIX_DEFAULTS) do - if hotfix_version_override then + local entry = hotfix_meta and hotfix_meta[key] + if entry ~= nil and pandoc.utils.type(entry) == 'table' then + -- Map form: { enabled: bool, quarto-version: "x.y.z" } + local enabled = true + if entry['enabled'] ~= nil then + enabled = pandoc.utils.stringify(entry['enabled']) == 'true' + end + local ver = entry['quarto-version'] + if ver then + ver = pandoc.utils.stringify(ver) + if ver ~= '' then + local ok, threshold = pcall(pandoc.types.Version, ver) + if ok and quarto.version >= threshold then + enabled = false + end + end + end + hotfix[key] = enabled + elseif entry ~= nil then + -- Simple boolean/string form + hotfix[key] = pandoc.utils.stringify(entry) == 'true' + elseif global_version_disabled then hotfix[key] = false - elseif hotfix_meta and hotfix_meta[key] ~= nil then - hotfix[key] = pandoc.utils.stringify(hotfix_meta[key]) == 'true' else hotfix[key] = default end @@ -487,9 +511,19 @@ function Meta(meta) typst_wrapper = opts['wrapper'], hotfix_code_annotations = hotfix['code-annotations'], hotfix_skylighting = hotfix['skylighting'], + hotfix_typst_title = hotfix['typst-title'], code_annotations = annotations_enabled, } + -- Store hotfix state in metadata so the post-quarto typst-title-fix filter + -- can read it (it runs as a separate filter and has no access to CONFIG). + if not meta['_code-window-hotfix'] then + meta['_code-window-hotfix'] = {} + end + meta['_code-window-hotfix']['typst-title'] = pandoc.MetaString( + hotfix['typst-title'] and 'true' or 'false' + ) + -- Cache syntax highlighting background colour for Typst contrast-aware annotations. if CURRENT_FORMAT == 'typst' then local hm = PANDOC_WRITER_OPTIONS and PANDOC_WRITER_OPTIONS.highlight_method