diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7ba8a..d61aee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Breaking Changes + +- refactor: Remove the `typst-title` hot-fix and its `hotfix.typst-title` option, as Quarto renders theorem titles as Typst markup since 1.10. (#78) +- build: Require Quarto 1.10.19 or later. (#78) + +### Bug Fixes + +- fix: Style inline code in a Typst theorem title like inline code anywhere else. (#78) + ## 1.6.0 (2026-10-05) ### New Features diff --git a/_extensions/code-window/_extension.yml b/_extensions/code-window/_extension.yml index 3c87631..4e45315 100644 --- a/_extensions/code-window/_extension.yml +++ b/_extensions/code-window/_extension.yml @@ -1,10 +1,8 @@ title: Code Window author: Mickaël Canouil version: 1.6.0 -quarto-required: ">=1.9.36" +quarto-required: ">=1.10.19" contributes: filters: - at: pre-quarto path: main.lua - - at: post-quarto - 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 a3a48d3..cc62269 100644 --- a/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua +++ b/_extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua @@ -3,11 +3,12 @@ --- @copyright 2026 Mickaël Canouil --- @author Mickaël Canouil --- @brief Typst skylighting styling override and inline code background. ---- Pandoc 3.8+ generates correct token colours and bgcolor from the theme, ---- but the generated Skylighting block lacks styling properties (width, inset, ---- radius, stroke) and ignores its own fill parameter. This module overrides ---- the Skylighting function with better block styling and adds inline code ---- background support for Typst output. +--- This module redefines the Skylighting function to draw code annotation +--- markers, and adds inline code background support for Typst output. +--- Quarto patches the block styling of its own Skylighting function since +--- 1.9.27 (https://github.com/quarto-dev/quarto-cli/issues/14126), but the +--- redefinition replaces that function, so it carries the block styling +--- (width, inset, radius, stroke) and honours its fill parameter itself. local _wrapper_prefix = 'code-window' @@ -18,10 +19,9 @@ local function set_wrapper(prefix) _wrapper_prefix = prefix end ---- Build a Skylighting override with improved block styling. ---- Pandoc 3.8+ generates correct bgcolor but the block call lacks width, ---- inset, radius, and stroke properties. The fill parameter is also ignored. ---- This override fixes both issues. +--- Build a Skylighting override that draws annotation markers. +--- It replaces Quarto's patched function, so it sets the block width, inset, +--- radius and stroke itself, and honours its fill parameter. --- @return string|nil Typst #let Skylighting(...) definition, or nil local function build_skylighting_override() local hm = PANDOC_WRITER_OPTIONS and PANDOC_WRITER_OPTIONS.highlight_method @@ -219,61 +219,16 @@ function Pandoc(doc) return doc end ---- Check if a Div is a Quarto title scaffold (inline-only content). ---- NOTE: relies on Quarto's internal `__quarto_custom_scaffold` attribute, ---- which is not part of the public API and may change without notice. ---- @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 - --- Walk the document tree and box inline Code with background styling. --- The extension contributes the pass once, so it sees each Code element ---- once and boxes it once. ---- 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. +--- once and boxes it once, theorem titles included. Metadata such as the +--- document title is left alone: the template writes it before the +--- definitions the box relies on. 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 = function(el) - return pandoc.RawInline('typst', '`' .. el.text .. '`') - end, - } - - 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) + doc.blocks = doc.blocks:walk({ Code = process_typst_inline }) return doc end diff --git a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua b/_extensions/code-window/_modules/hotfix/typst-title-fix.lua deleted file mode 100644 index 89c3690..0000000 --- a/_extensions/code-window/_modules/hotfix/typst-title-fix.lua +++ /dev/null @@ -1,111 +0,0 @@ ---- @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 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 -#let %s(title: none, ..args) = { - let t = if title != none and type(title) == str { - eval(title, mode: "markup") - } else { - 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 ---- @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 - ---- Scan source files for cross-reference div IDs and return the ---- corresponding Typst function names. ---- NOTE: uses raw-text pattern matching on source files, so it will not ---- detect divs inside `include` shortcodes or other indirect sources. ---- @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 ok, source = pcall(f.read, f, '*a') - f:close() - if not ok then source = '' end - 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) - if not quarto.doc.is_format('typst') then - 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' - and blk.text:find('code-window: hot-fix for Quarto rendering theorem titles', 1, true) then - return doc - end - end - - local func_names = detect_theorem_types() - if #func_names == 0 then - return doc - end - - -- 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. - -- This relies on Quarto emitting the template preamble before - -- doc.blocks; if that ordering changes the wrappers will break. - table.insert(doc.blocks, 1, pandoc.RawBlock('typst', build_wrappers(func_names))) - - return doc - end, - }, -} diff --git a/_extensions/code-window/_schema.yml b/_extensions/code-window/_schema.yml index b7938d9..2484de6 100644 --- a/_extensions/code-window/_schema.yml +++ b/_extensions/code-window/_schema.yml @@ -60,18 +60,6 @@ options: quarto-version: type: string description: "Quarto version at or above which this hot-fix is automatically disabled." - typst-title: - type: [boolean, object] - default: true - description: "Enable the Typst title hot-fix (evaluates theorem title strings as markup). Use a boolean or a map with 'enabled' and 'quarto-version' keys." - additionalProperties: false - properties: - enabled: - type: boolean - description: "Explicitly enable or disable this hot-fix." - quarto-version: - type: string - description: "Quarto version at or above which this hot-fix is automatically disabled." attributes: CodeBlock: diff --git a/_extensions/code-window/code-window.lua b/_extensions/code-window/code-window.lua index d67c3c0..b1cf221 100644 --- a/_extensions/code-window/code-window.lua +++ b/_extensions/code-window/code-window.lua @@ -76,7 +76,6 @@ local VALID_COLLAPSE = { local HOTFIX_DEFAULTS = { ['code-annotations'] = true, ['skylighting'] = true, - ['typst-title'] = true, } local CURRENT_FORMAT = nil @@ -919,19 +918,9 @@ function Meta(meta) lines_label = opts['lines-label'] == 'true', 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'] = pandoc.MetaMap({}) - end - meta['_code-window-hotfix']['typst-title'] = pandoc.MetaString( - CONFIG.enabled and 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 diff --git a/docs/examples.qmd b/docs/examples.qmd index 0abce96..a0a71b2 100644 --- a/docs/examples.qmd +++ b/docs/examples.qmd @@ -228,7 +228,7 @@ The option does nothing in HTML, which draws its chrome from the stylesheet and ## The Typst hot-fixes -The three hot-fixes act on Typst output only. +The two hot-fixes act on Typst output only. Each one is on by default. A boolean turns one off: @@ -238,7 +238,6 @@ extensions: hotfix: code-annotations: true skylighting: false - typst-title: true ``` A map adds a version at which the hot-fix switches itself off: @@ -249,7 +248,7 @@ extensions: hotfix: skylighting: enabled: true - quarto-version: "1.9.0" + quarto-version: "1.11.0" ``` The extension compares that string with the running Quarto. diff --git a/docs/reference.qmd b/docs/reference.qmd index e75f844..c560b9e 100644 --- a/docs/reference.qmd +++ b/docs/reference.qmd @@ -119,7 +119,6 @@ All of them follow `enabled`, so a document that turns the filter off takes no h | --- | --- | | `code-annotations` | Code annotations in Typst output. | | `skylighting` | Block styling and inline code backgrounds in Typst output. | -| `typst-title` | Theorem title strings, evaluated as markup. | : The Typst hot-fixes. {.striped .hover tbl-colwidths="[26,74]"} @@ -129,7 +128,7 @@ extensions: hotfix: skylighting: enabled: true - quarto-version: "1.9.0" + quarto-version: "1.11.0" ``` They exist for [quarto-dev/quarto-cli#14170](https://github.com/quarto-dev/quarto-cli/issues/14170) and will be removed once Quarto covers the same ground. diff --git a/example.qmd b/example.qmd index 061edcd..ebe109c 100644 --- a/example.qmd +++ b/example.qmd @@ -275,7 +275,6 @@ extensions: hotfix: code-annotations: true skylighting: true - typst-title: true ``` Each hotfix accepts either a boolean or a map with `enabled` and `quarto-version` keys for per-hotfix version thresholds: @@ -284,11 +283,10 @@ Each hotfix accepts either a boolean or a map with `enabled` and `quarto-version extensions: code-window: hotfix: - code-annotations: true + code-annotations: + quarto-version: "1.11.0" skylighting: enabled: false - typst-title: - quarto-version: "1.10.0" ``` Override per block with the `code-window-style` attribute: diff --git a/tests/fixtures/theorem-title.qmd b/tests/fixtures/theorem-title.qmd new file mode 100644 index 0000000..e31a818 --- /dev/null +++ b/tests/fixtures/theorem-title.qmd @@ -0,0 +1,17 @@ +--- +title: "Theorem title" +filters: + - code-window +format: + typst: + keep-typ: true +--- + +A theorem title holds markup and inline code. +Quarto renders the title as Typst content, so no hot-fix is needed. + +::: {#thm-markup} +## A title with *emphasis* and `code` + +Body. +::: diff --git a/tests/fixtures/title-inline-code.qmd b/tests/fixtures/title-inline-code.qmd new file mode 100644 index 0000000..e55e5a4 --- /dev/null +++ b/tests/fixtures/title-inline-code.qmd @@ -0,0 +1,13 @@ +--- +title: "A `code` title" +filters: + - code-window +format: + typst: + keep-typ: true + highlight-style: none +--- + +The document title holds inline code, and the theme gives no background. +The title is metadata, which the inline code pass leaves alone, so the render +compiles. diff --git a/tests/run.sh b/tests/run.sh index 8ae852c..0bc9c75 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -345,6 +345,51 @@ else 'DEFAULT_COLLAPSE="closed" in collapse-block-off.html' fi +# ============================================================================ +# Inline code in the document title is left to Quarto +# ============================================================================ + +# The title is metadata, written into the template call before the +# extension's own definitions, so the inline code pass must not box it. The +# render compiling is the check; the grep says what reached the title. +render title-inline-code typst +# shellcheck disable=SC2016 # the backticks are a literal Typst code span, not a command +if grep -q 'title: \[A #raw("code") title\]\|title: \[A `code` title\]' "${work_dir}/title-inline-code.typ"; then + report pass "title-inline-code: the title code is not boxed" +else + report fail "title-inline-code: the title code is not boxed" \ + "an unboxed code span in the title argument of title-inline-code.typ" +fi + +# ============================================================================ +# A theorem title renders as Typst content without a hot-fix +# ============================================================================ + +# Quarto writes the title as content since 1.10, so the markup survives and +# the extension adds no wrapper of its own. Inline code in the title still has +# to compile, which the render itself checks. +render theorem-title typst +if grep -qF '#theorem(title: [A title with #emph[emphasis]' "${work_dir}/theorem-title.typ"; then + report pass "theorem-title: the title keeps its markup" +else + report fail "theorem-title: the title keeps its markup" \ + "#theorem(title: [A title with #emph[emphasis] in theorem-title.typ" +fi +if grep -q 'hot-fix for Quarto rendering theorem titles' "${work_dir}/theorem-title.typ"; then + report fail "theorem-title: no title hot-fix is injected" \ + "no 'hot-fix for Quarto rendering theorem titles' block in theorem-title.typ" +else + report pass "theorem-title: no title hot-fix is injected" +fi + +# Inline code in the title takes the same box as inline code anywhere else. +if grep -F '#theorem(title: [A title with #emph[emphasis]' "${work_dir}/theorem-title.typ" | grep -q 'box(fill:'; then + report pass "theorem-title: inline code in the title is boxed" +else + report fail "theorem-title: inline code in the title is boxed" \ + "a box(fill: call inside the #theorem(title: [...]) line of theorem-title.typ" +fi + # ============================================================================ printf '\n%s passed, %s failed\n' "${passed}" "${failed}"