Skip to content

Commit 905e680

Browse files
authored
refactor: consolidate hotfix options under nested key (#7)
1 parent 9faa75a commit 905e680

12 files changed

Lines changed: 946 additions & 873 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ docs/_sidebar-api.yml
66
docs/api/*.qmd
77
*.html
88
.quarto/
9-
109
/.luarc.json
10+
/example.typ
11+
/example-typst.pdf

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@
55
### New Features
66

77
- feat: add multiple window decoration styles for code blocks (`macos`, `windows`, `default`).
8-
- feat: add `skylighting-fix` option to enable or disable the Skylighting hot-fix for Typst output (enabled by default).
8+
- feat: add Typst code-annotations hot-fix with annotation markers, circled numbers, and bidirectional linking.
9+
- feat: add `hotfix.quarto-version` threshold to auto-disable temporary hot-fixes when Quarto reaches a specified version.
10+
11+
### Bug Fixes
12+
13+
- fix: HTML-escape auto-generated filename in code block headers to prevent XSS.
14+
- fix: skylighting hot-fix now respects custom `wrapper` name instead of hardcoding `code-window-circled-number`.
915

1016
### Style
1117

1218
- style: adjust padding and height for title bar in code window.
1319

1420
### Refactoring
1521

22+
- refactor: consolidate `skylighting-fix` option into nested `hotfix` configuration key with `code-annotations` and `skylighting` toggles.
23+
- refactor: introduce `main.lua` entry point for filter assembly and dependency wiring.
24+
- refactor: move hotfix modules (`code-annotations.lua`, `skylighting-typst-fix.lua`) into `_modules/hotfix/`.
25+
- refactor: split Typst function definitions so annotation helpers are only injected when at least one hot-fix is active.
1626
- refactor: update Typst processing to return block sandwich.
1727
- refactor: use utility functions for code-window extension.
1828

README.md

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,30 @@ extensions:
5858
auto-filename: true
5959
style: "macos"
6060
wrapper: "code-window"
61-
skylighting-fix: true
61+
hotfix:
62+
quarto-version: ~
63+
code-annotations: true
64+
skylighting: true
6265
```
6366

6467
### Options
6568

66-
| Option | Type | Default | Description |
67-
| ----------------- | ------- | --------------- | ----------------------------------------------------------------------------------- |
68-
| `enabled` | boolean | `true` | Enable or disable the code-window filter. |
69-
| `auto-filename` | boolean | `true` | Automatically generate filename labels from the code block language. |
70-
| `style` | string | `"macos"` | Window decoration style: `"macos"`, `"windows"`, or `"default"`. |
71-
| `wrapper` | string | `"code-window"` | Typst wrapper function name for code-window rendering. |
72-
| `skylighting-fix` | boolean | `true` | Enable or disable the Skylighting hot-fix for Typst output (block and inline code). |
69+
| Option | Type | Default | Description |
70+
| --------------- | ------- | --------------- | -------------------------------------------------------------------- |
71+
| `enabled` | boolean | `true` | Enable or disable the code-window filter. |
72+
| `auto-filename` | boolean | `true` | Automatically generate filename labels from the code block language. |
73+
| `style` | string | `"macos"` | Window decoration style: `"macos"`, `"windows"`, or `"default"`. |
74+
| `wrapper` | string | `"code-window"` | Typst wrapper function name for code-window rendering. |
75+
76+
### Hotfix Options
77+
78+
These options are **temporary** and will be removed in a future version (see [Temporary hot-fixes](#temporary-hot-fixes-typst)).
79+
80+
| Option | Type | Default | Description |
81+
| ------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------ |
82+
| `hotfix.quarto-version` | string | _unset_ | Quarto version at or above which all hot-fixes are automatically disabled. |
83+
| `hotfix.code-annotations` | boolean | `true` | Enable the code-annotations hot-fix for Typst output. |
84+
| `hotfix.skylighting` | boolean | `true` | Enable the Skylighting hot-fix for Typst output (overrides block styling and inline code). |
7385

7486
### Styles
7587

@@ -87,17 +99,32 @@ print("Windows style for this block only")
8799
```
88100
````
89101

90-
### Typst Skylighting Hot-fix (Integrated)
102+
### Temporary Hot-fixes (Typst)
103+
104+
The extension includes two temporary hot-fixes for Typst output that compensate for missing Quarto/Pandoc features.
105+
Both will be removed once [quarto-dev/quarto-cli#14170](https://github.com/quarto-dev/quarto-cli/pull/14170) is released.
106+
After that, the extension will focus solely on **auto-filename** and **code-window-style** features.
107+
108+
- **`hotfix.code-annotations`**: processes code annotation markers for Typst, since Quarto does not yet support `code-annotations` in Typst output.
109+
The `filename` attribute for code blocks will also become natively supported.
110+
- **`hotfix.skylighting`**: overrides Pandoc's Skylighting output for Typst to fix block and inline code styling.
91111

92-
`code-window` loads its Typst skylighting hot-fix internally from `_extensions/code-window/skylighting-typst-fix.lua`, so no second filter entry is required.
93-
Set `skylighting-fix: false` to disable the hot-fix without removing the file.
112+
Set `hotfix.quarto-version` to automatically disable both hot-fixes once you update Quarto to the version that includes native support:
94113

95-
This keeps the hot-fix separated from `code-window.lua` for easy future removal while preserving combined behaviour.
114+
```yaml
115+
extensions:
116+
code-window:
117+
hotfix:
118+
quarto-version: "1.10.0"
119+
```
96120

97121
Future removal playbook:
98122

99-
1. Remove the skylighting loader call in `_extensions/code-window/code-window.lua`.
100-
2. Delete `_extensions/code-window/skylighting-typst-fix.lua`.
123+
1. Delete `hotfix` parsing from `code-window.lua` (`HOTFIX_DEFAULTS`, hotfix section in `Meta`).
124+
2. Remove the `hotfix` section from `_schema.yml`.
125+
3. Remove the skylighting guard and loader in `main.lua`.
126+
4. Remove annotation processing from `code-window.lua`.
127+
5. Delete `_modules/hotfix/` directory entirely.
101128

102129
## Example
103130

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
title: Code Window
22
author: Mickaël Canouil
33
version: 0.2.0
4-
quarto-required: ">=1.9.23"
4+
quarto-required: ">=1.9.36"
55
contributes:
66
filters:
77
- at: pre-quarto
8-
path: code-window.lua
8+
path: main.lua
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
--- @module code-annotations
2+
--- @license MIT
3+
--- @copyright 2026 Mickaël Canouil
4+
--- @author Mickaël Canouil
5+
--- @brief Code annotation detection, stripping, and Typst rendering helpers.
6+
--- Scans CodeBlock elements for inline annotation markers (e.g. # <1>, // <2>)
7+
--- and provides utilities for converting annotations to Typst output.
8+
9+
-- ============================================================================
10+
-- LANGUAGE COMMENT CHARACTERS
11+
-- ============================================================================
12+
13+
--- Map of language identifiers to their single-line comment prefix.
14+
--- @type table<string, string>
15+
local LANG_COMMENT_CHARS = {
16+
r = '#',
17+
python = '#',
18+
lua = '--',
19+
javascript = '//',
20+
typescript = '//',
21+
go = '//',
22+
rust = '//',
23+
bash = '#',
24+
sh = '#',
25+
zsh = '#',
26+
fish = '#',
27+
c = '//',
28+
cpp = '//',
29+
cxx = '//',
30+
cc = '//',
31+
cs = '//',
32+
java = '//',
33+
scala = '//',
34+
kotlin = '//',
35+
swift = '//',
36+
objc = '//',
37+
php = '//',
38+
ruby = '#',
39+
perl = '#',
40+
julia = '#',
41+
haskell = '--',
42+
elm = '--',
43+
clojure = ';',
44+
scheme = ';',
45+
lisp = ';',
46+
racket = ';',
47+
erlang = '%%',
48+
elixir = '#',
49+
fortran = '!',
50+
matlab = '%%',
51+
ada = '--',
52+
sql = '--',
53+
plsql = '--',
54+
tsql = '--',
55+
mysql = '--',
56+
sqlite = '--',
57+
postgresql = '--',
58+
vb = "'",
59+
vbnet = "'",
60+
fsharp = '//',
61+
stata = '//',
62+
yaml = '#',
63+
toml = '#',
64+
make = '#',
65+
cmake = '#',
66+
dockerfile = '#',
67+
powershell = '#',
68+
nix = '#',
69+
zig = '//',
70+
dart = '//',
71+
groovy = '//',
72+
d = '//',
73+
nim = '#',
74+
crystal = '#',
75+
v = '//',
76+
odin = '//',
77+
mojo = '#',
78+
}
79+
80+
-- ============================================================================
81+
-- ANNOTATION RESOLUTION
82+
-- ============================================================================
83+
84+
--- Escape a string for use in a Lua pattern.
85+
--- @param s string
86+
--- @return string
87+
local function escape_pattern(s)
88+
return s:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1')
89+
end
90+
91+
--- Resolve annotations in a CodeBlock element.
92+
--- Scans each line for a trailing annotation marker (e.g. # <1>) using the
93+
--- language's comment prefix. Strips the marker from the code text and returns
94+
--- the cleaned text along with an annotations table.
95+
--- @param block pandoc.CodeBlock
96+
--- @return string cleaned_text The code with annotation markers removed
97+
--- @return table|nil annotations Maps line numbers (int) to annotation numbers (int), or nil if none found
98+
local function resolve_annotations(block)
99+
if not block.classes or #block.classes == 0 then
100+
return block.text, nil
101+
end
102+
103+
local lang = block.classes[1]:lower()
104+
local comment = LANG_COMMENT_CHARS[lang]
105+
if not comment then
106+
return block.text, nil
107+
end
108+
109+
local escaped_comment = escape_pattern(comment)
110+
local pattern = '^(.-)%s*' .. escaped_comment .. '%s*<%s*(%d+)%s*>%s*$'
111+
112+
local annotations = {}
113+
local lines = {}
114+
local found = false
115+
116+
local line_num = 0
117+
for line in (block.text .. '\n'):gmatch('([^\n]*)\n') do
118+
line_num = line_num + 1
119+
local content, annot_num = line:match(pattern)
120+
if annot_num then
121+
found = true
122+
annotations[line_num] = tonumber(annot_num)
123+
table.insert(lines, content)
124+
else
125+
table.insert(lines, line)
126+
end
127+
end
128+
129+
if not found then
130+
return block.text, nil
131+
end
132+
133+
return table.concat(lines, '\n'), annotations
134+
end
135+
136+
-- ============================================================================
137+
-- TYPST CONVERSION HELPERS
138+
-- ============================================================================
139+
140+
--- Convert an annotations table to a Typst dictionary literal.
141+
--- Keys are stringified line numbers, values are annotation numbers.
142+
--- Example output: (1: 2, 3: 1)
143+
--- @param annotations table<int, int> Line number to annotation number mapping
144+
--- @return string Typst dictionary literal
145+
local function annotations_to_typst_dict(annotations)
146+
local pairs_list = {}
147+
local keys = {}
148+
for k in pairs(annotations) do
149+
table.insert(keys, k)
150+
end
151+
table.sort(keys)
152+
for _, line_num in ipairs(keys) do
153+
table.insert(pairs_list,
154+
string.format('"%d": %d', line_num, annotations[line_num]))
155+
end
156+
return '(' .. table.concat(pairs_list, ', ') .. ')'
157+
end
158+
159+
--- Check whether a block is an OrderedList that looks like an annotation list.
160+
--- Annotation lists are OrderedLists immediately following a code block,
161+
--- where each item corresponds to an annotation number.
162+
--- @param block pandoc.Block
163+
--- @return boolean
164+
local function is_annotation_ordered_list(block)
165+
return block and block.t == 'OrderedList'
166+
end
167+
168+
--- Convert an OrderedList to Typst annotation item RawBlocks.
169+
--- Each list item becomes a #code-window-annotation-item(block-id, n)[...] call.
170+
--- @param ol pandoc.OrderedList The ordered list to convert
171+
--- @param wrapper_prefix string Prefix for the Typst function name
172+
--- @param block_id integer Unique block identifier for bidirectional linking
173+
--- @return pandoc.List List of RawBlock elements
174+
local function ordered_list_to_typst_blocks(ol, wrapper_prefix, block_id)
175+
local blocks = {}
176+
local start = ol.listAttributes and ol.listAttributes.start or 1
177+
for i, item in ipairs(ol.content) do
178+
local annot_num = start + i - 1
179+
local content_blocks = pandoc.Blocks(item)
180+
local rendered = pandoc.write(pandoc.Pandoc(content_blocks), 'typst')
181+
rendered = rendered:gsub('%s+$', '')
182+
table.insert(blocks, pandoc.RawBlock('typst', string.format(
183+
'#%s-annotation-item(%d, %d)[%s]',
184+
wrapper_prefix, block_id, annot_num, rendered
185+
)))
186+
end
187+
return blocks
188+
end
189+
190+
-- ============================================================================
191+
-- MODULE EXPORTS
192+
-- ============================================================================
193+
194+
return {
195+
LANG_COMMENT_CHARS = LANG_COMMENT_CHARS,
196+
resolve_annotations = resolve_annotations,
197+
annotations_to_typst_dict = annotations_to_typst_dict,
198+
is_annotation_ordered_list = is_annotation_ordered_list,
199+
ordered_list_to_typst_blocks = ordered_list_to_typst_blocks,
200+
}

0 commit comments

Comments
 (0)