Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
### Bug Fixes

- fix: unwrap Quarto's `DecoratedCodeBlock` Div to prevent double filename wrapping in Typst output.
- 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.

### New Features

- feat: replace global `hotfix.quarto-version` with per-hotfix thresholds for independent auto-disable.

### Refactoring

- refactor: extract language normalisation into dedicated `_modules/language.lua` module.
Expand Down
41 changes: 30 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ extensions:
style: "macos"
wrapper: "code-window"
hotfix:
quarto-version: ~
code-annotations: true
skylighting: true
typst-title: true
```

### Options
Expand All @@ -77,11 +77,13 @@ extensions:

These options are **temporary** and will be removed in a future version (see [Temporary hot-fixes](#temporary-hot-fixes-typst)).

| Option | Type | Default | Description |
| ------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------ |
| `hotfix.quarto-version` | string | _unset_ | Quarto version at or above which all hot-fixes are automatically disabled. |
| `hotfix.code-annotations` | boolean | `true` | Enable the code-annotations hot-fix for Typst output. |
| `hotfix.skylighting` | boolean | `true` | Enable the Skylighting hot-fix for Typst output (overrides block styling and inline code). |
Each hotfix value can be a simple boolean or a map with `enabled` and `quarto-version` keys for per-hotfix version thresholds.

| Option | Type | Default | Description |
| ------------------------- | ------------ | ------- | ------------------------------------------------------------------------------------------ |
| `hotfix.code-annotations` | boolean/map | `true` | Enable the code-annotations hot-fix for Typst output. |
| `hotfix.skylighting` | boolean/map | `true` | Enable the Skylighting hot-fix for Typst output (overrides block styling and inline code). |
| `hotfix.typst-title` | boolean/map | `true` | Enable the Typst title hot-fix (evaluates theorem title strings as markup). |

### Styles

Expand All @@ -101,21 +103,37 @@ print("Windows style for this block only")

### Temporary Hot-fixes (Typst)

The extension includes two temporary hot-fixes for Typst output that compensate for missing Quarto/Pandoc features.
Both will be removed once [quarto-dev/quarto-cli#14170](https://github.com/quarto-dev/quarto-cli/pull/14170) is released.
The extension includes three temporary hot-fixes for Typst output that compensate for missing Quarto/Pandoc features.
All three will be removed once [quarto-dev/quarto-cli#14170](https://github.com/quarto-dev/quarto-cli/pull/14170) is released.
After that, the extension will focus solely on **auto-filename** and **code-window-style** features.

- **`hotfix.code-annotations`**: processes code annotation markers for Typst, since Quarto does not yet support `code-annotations` in Typst output.
The `filename` attribute for code blocks will also become natively supported.
- **`hotfix.skylighting`**: overrides Pandoc's Skylighting output for Typst to fix block and inline code styling.
- **`hotfix.typst-title`**: evaluates theorem title strings as Typst markup so that inline formatting (e.g., code) renders correctly.

Each hotfix can specify its own `quarto-version` threshold, since upstream fixes may land in different Quarto releases:

```yaml
extensions:
code-window:
hotfix:
code-annotations:
quarto-version: "1.10.0"
skylighting:
quarto-version: "1.11.0"
typst-title:
quarto-version: "1.10.0"
```

Set `hotfix.quarto-version` to automatically disable both hot-fixes once you update Quarto to the version that includes native support:
The map form also accepts an `enabled` key to explicitly enable or disable a hotfix regardless of version:

```yaml
extensions:
code-window:
hotfix:
quarto-version: "1.10.0"
skylighting:
enabled: false
```

Future removal playbook:
Expand All @@ -124,7 +142,8 @@ Future removal playbook:
2. Remove the `hotfix` section from `_schema.yml`.
3. Remove the skylighting guard and loader in `main.lua`.
4. Remove annotation processing from `code-window.lua`.
5. Delete `_modules/hotfix/` directory entirely.
5. Remove the typst-title fix filter and metadata bridge.
6. Delete `_modules/hotfix/` directory entirely.

## Example

Expand Down
2 changes: 2 additions & 0 deletions _extensions/code-window/_extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ contributes:
filters:
- at: pre-quarto
path: main.lua
- at: post-quarto
path: _modules/hotfix/typst-title-fix.lua
58 changes: 53 additions & 5 deletions _extensions/code-window/_modules/hotfix/skylighting-typst-fix.lua
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,66 @@ 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).
--- 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 convert inline Code to RawInline with
--- 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 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 = 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)
return doc
end

return {
set_wrapper = set_wrapper,
filters = {
{ Pandoc = Pandoc },
{ Code = Code },
{ Pandoc = process_inline_code },
},
}
111 changes: 111 additions & 0 deletions _extensions/code-window/_modules/hotfix/typst-title-fix.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
--- @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,
},
}
36 changes: 29 additions & 7 deletions _extensions/code-window/_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,39 @@ options:
type: object
description: "Temporary hot-fixes for Typst output. These will be removed when Quarto natively supports the corresponding features (see quarto-dev/quarto-cli#14170)."
properties:
quarto-version:
type: string
description: "Quarto version at or above which all hot-fixes are automatically disabled. Leave unset to use individual toggles."
code-annotations:
type: boolean
type: [boolean, object]
default: true
description: "Enable the code-annotations hot-fix for Typst output."
description: "Enable the code-annotations hot-fix for Typst output. Use a boolean or a map with 'enabled' and 'quarto-version' keys."
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."
skylighting:
type: boolean
type: [boolean, object]
default: true
description: "Enable the Skylighting hot-fix for Typst output (overrides block styling and adds inline code background)."
description: "Enable the Skylighting hot-fix for Typst output (overrides block styling and adds inline code background). Use a boolean or a map with 'enabled' and 'quarto-version' keys."
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."
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."
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:
Expand Down
Loading