Skip to content

Commit b9eaaee

Browse files
fix: Skylighting code block styling for Typst (#14126)
A TypeScript postprocessor patches the Pandoc-generated Skylighting function in .typ output to add width: 100%, inset: 8pt, and radius: 2pt to the block call, matching native code block styling. The postprocessor matches the full Skylighting function signature to ensure only the intended code is modified. For brand documents, also resolves monospace-block background-color (including palette color names) and overrides the bgcolor value. Fixes #14126
1 parent 3f3f341 commit b9eaaee

17 files changed

Lines changed: 451 additions & 0 deletions

File tree

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ All changes included in 1.9:
8383
- ([#13954](https://github.com/quarto-dev/quarto-cli/issues/13954)): Add support for Typst book projects via format extensions. Quarto now bundles the `orange-book` extension which provides a textbook-style format with chapter numbering, cross-references, and professional styling. Book projects with `format: typst` automatically use this extension.
8484
- ([#13978](https://github.com/quarto-dev/quarto-cli/pull/13978)): Keep term and description together in definition lists to avoid breaking across pages. (author: @mcanouil)
8585
- ([#13878](https://github.com/quarto-dev/quarto-cli/issues/13878)): Typst now uses Pandoc's skylighting for syntax highlighting by default (consistent with other formats). Use `syntax-highlighting: idiomatic` to opt-in to Typst's native syntax highlighting instead.
86+
- ([#14126](https://github.com/quarto-dev/quarto-cli/issues/14126)): Fix Skylighting code blocks in Typst lacking full-width background, padding, and border radius. A postprocessor patches the Pandoc-generated Skylighting function to add `width: 100%`, `inset: 8pt`, and `radius: 2pt` to the block call, matching the styling of native code blocks. Brand `monospace-block.background-color` also now correctly applies to Skylighting output. This workaround will be removed once the fix is upstreamed to Skylighting.
8687

8788
### `pdf`
8889

src/format/typst/format-typst.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import { RenderServices } from "../../command/render/types.ts";
1010
import { ProjectContext } from "../../project/types.ts";
1111
import { BookExtension } from "../../project/types/book/book-shared.ts";
1212
import {
13+
kBrand,
1314
kCiteproc,
1415
kColumns,
1516
kDefaultImageExtension,
1617
kFigFormat,
1718
kFigHeight,
1819
kFigWidth,
20+
kLight,
1921
kLogo,
2022
kNumberSections,
2123
kSectionNumbering,
@@ -27,6 +29,7 @@ import {
2729
Format,
2830
FormatExtras,
2931
FormatPandoc,
32+
LightDarkBrand,
3033
Metadata,
3134
PandocFlags,
3235
} from "../../config/types.ts";
@@ -142,15 +145,86 @@ export function typstFormat(): Format {
142145
].map((partial) => join(templateDir, partial)),
143146
};
144147

148+
// Postprocessor to fix Skylighting code block styling (issue #14126).
149+
// Pandoc's generated Skylighting function uses block(fill: bgcolor, blocks)
150+
// which lacks width, inset, and radius. We surgically fix this in the .typ
151+
// output. If brand monospace-block has a background-color, we also override
152+
// the bgcolor value.
153+
const brandData = (format.render[kBrand] as LightDarkBrand | undefined)
154+
?.[kLight];
155+
const monospaceBlock = brandData?.processedData?.typography?.[
156+
"monospace-block"
157+
];
158+
let brandBgColor = (monospaceBlock && typeof monospaceBlock !== "string")
159+
? monospaceBlock["background-color"] as string | undefined
160+
: undefined;
161+
// Resolve palette color names (e.g. "code-bg" → "#1e1e2e")
162+
if (brandBgColor && brandData?.data?.color?.palette) {
163+
const palette = brandData.data.color.palette as Record<string, string>;
164+
let resolved = brandBgColor;
165+
while (palette[resolved]) {
166+
resolved = palette[resolved];
167+
}
168+
brandBgColor = resolved;
169+
}
170+
145171
return {
146172
pandoc,
147173
metadata,
148174
templateContext,
175+
postprocessors: [
176+
skylightingPostProcessor(brandBgColor),
177+
],
149178
};
150179
},
151180
});
152181
}
153182

183+
// Fix Skylighting code block styling in .typ output (issue #14126).
184+
// The Pandoc-generated Skylighting function uses block(fill: bgcolor, blocks)
185+
// which lacks width, inset, and radius. This postprocessor matches the entire
186+
// Skylighting function by its distinctive signature and patches only within it.
187+
// When brand provides a monospace-block background-color, also overrides the
188+
// bgcolor value. This is a temporary workaround until the fix is upstreamed
189+
// to the Skylighting library.
190+
function skylightingPostProcessor(brandBgColor?: string) {
191+
// Match the entire #let Skylighting(...) = { ... } function.
192+
// The signature is stable and generated by Skylighting's Typst backend.
193+
const skylightingFnRe =
194+
/(#let Skylighting\(fill: none, number: false, start: 1, sourcelines\) = \{[\s\S]*?\n\})/;
195+
196+
return async (output: string) => {
197+
const content = Deno.readTextFileSync(output);
198+
199+
const match = skylightingFnRe.exec(content);
200+
if (!match) {
201+
// No Skylighting function found — document may not have code blocks,
202+
// or upstream changed the function signature. Nothing to patch.
203+
return;
204+
}
205+
206+
let fn = match[1];
207+
208+
// Fix block() call: add width, inset, radius
209+
fn = fn.replace(
210+
"block(fill: bgcolor, blocks)",
211+
"block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks)",
212+
);
213+
214+
// Override bgcolor with brand monospace-block background-color
215+
if (brandBgColor) {
216+
fn = fn.replace(
217+
/let bgcolor = rgb\("[^"]*"\)/,
218+
`let bgcolor = rgb("${brandBgColor}")`,
219+
);
220+
}
221+
222+
if (fn !== match[1]) {
223+
Deno.writeTextFileSync(output, content.replace(match[1], fn));
224+
}
225+
};
226+
}
227+
154228
function typstResolveFormat(format: Format) {
155229
// Pandoc citeproc with typst output requires adjustment
156230
// https://github.com/jgm/pandoc/commit/e89a3edf24a025d5bb0fe8c4c7a8e6e0208fa846

src/resources/filters/quarto-post/typst-brand-yaml.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ function render_typst_brand_yaml()
250250
}))
251251
end
252252
end
253+
253254
end,
254255
Meta = function(meta)
255256
local brand = param('brand')
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
color:
2+
palette:
3+
code-fg: "#2d3748"
4+
5+
typography:
6+
monospace-block:
7+
color: code-fg
8+
weight: 500
9+
size: 10pt
10+
line-height: 1.5
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: Brand Monospace Block without Background Color
3+
format:
4+
typst:
5+
keep-typ: true
6+
_quarto:
7+
tests:
8+
typst:
9+
ensureTypstFileRegexMatches:
10+
-
11+
# Skylighting is active (default)
12+
- "#Skylighting"
13+
- "#KeywordTok"
14+
# Brand monospace-block text properties emitted as show rules
15+
- '^#show raw\.where\(block: true\): set text\(weight: 500, size: 10pt, fill: rgb\("#2d3748"\), \)$'
16+
- '^#show raw\.where\(block: true\): set par\(leading: 0\.75em\)$'
17+
# Even without brand bg, Skylighting override uses theme bgcolor
18+
# so that width/inset/radius are applied
19+
- 'let bgcolor = rgb\("#f1f3f5"\)'
20+
- 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)'
21+
# No brand background-color show rule (not configured)
22+
- ['^#show raw\.where\(block: true\): set block\(fill:']
23+
---
24+
25+
Brand sets monospace-block color, weight, size, and line-height but NOT
26+
background-color. The Skylighting override should still be emitted using
27+
the theme's background color so that code blocks get proper width/inset/radius.
28+
29+
```python
30+
def hello():
31+
x = 1 + 2
32+
print(f"result: {x}")
33+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
color:
2+
palette:
3+
code-bg: "#1e1e2e"
4+
code-fg: "#cdd6f4"
5+
6+
typography:
7+
monospace-block:
8+
color: code-fg
9+
background-color: code-bg
10+
size: 10pt
11+
weight: 400
12+
line-height: 1.6
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: Brand Monospace Block with Skylighting
3+
format:
4+
typst:
5+
keep-typ: true
6+
_quarto:
7+
tests:
8+
typst:
9+
ensureTypstFileRegexMatches:
10+
-
11+
# Skylighting is active (default)
12+
- "#Skylighting"
13+
- "#KeywordTok"
14+
# Brand monospace-block properties are emitted as show rules
15+
# (still useful for idiomatic mode fallback)
16+
- '^#show raw\.where\(block: true\): set text\(weight: 400, size: 10pt, fill: rgb\("#cdd6f4"\), \)$'
17+
- '^#show raw\.where\(block: true\): set block\(fill: rgb\("#1e1e2e"\)\)$'
18+
- '^#show raw\.where\(block: true\): set par\(leading: 0\.85em\)$'
19+
# Quarto-generated Skylighting override with brand bg and proper block styling
20+
- 'let bgcolor = rgb\("#1e1e2e"\)'
21+
- 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)'
22+
# Should NOT have raw fenced blocks
23+
- ["```python"]
24+
---
25+
26+
Brand monospace-block options should apply to Skylighting code blocks.
27+
28+
```python
29+
def hello():
30+
x = 1 + 2
31+
print(f"result: {x}")
32+
```
33+
34+
Inline code like `hello()`{.python} should NOT get monospace-block styling.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
color:
2+
palette:
3+
block-bg: "#f0f4f8"
4+
block-fg: "#1a365d"
5+
inline-bg: "#fed7d7"
6+
inline-fg: "#9b2c2c"
7+
8+
typography:
9+
monospace-block:
10+
color: block-fg
11+
background-color: block-bg
12+
size: 11pt
13+
weight: 400
14+
line-height: 1.5
15+
monospace-inline:
16+
color: inline-fg
17+
background-color: inline-bg
18+
weight: 600
19+
size: 0.9rem
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: Brand Monospace with Idiomatic Highlighting
3+
format:
4+
typst:
5+
keep-typ: true
6+
syntax-highlighting: idiomatic
7+
_quarto:
8+
tests:
9+
typst:
10+
ensureTypstFileRegexMatches:
11+
-
12+
# Idiomatic = native typst highlighting = raw fenced code blocks
13+
- "```python"
14+
# Brand monospace-block properties (these target raw.where(block: true)
15+
# which DOES match native/idiomatic code blocks)
16+
- '^#show raw\.where\(block: true\): set text\(weight: 400, size: 11pt, fill: rgb\("#1a365d"\), \)$'
17+
- '^#show raw\.where\(block: true\): set block\(fill: rgb\("#f0f4f8"\)\)$'
18+
- '^#show raw\.where\(block: true\): set par\(leading: 0\.75em\)$'
19+
# Brand monospace-inline properties
20+
- '^#show raw\.where\(block: false\): set text\(weight: 600, size: 0\.9em, fill: rgb\("#9b2c2c"\), \)$'
21+
- '^#show raw\.where\(block: false\): content => highlight\(fill: rgb\("#fed7d7"\), content\)$'
22+
# Should NOT have Skylighting tokens
23+
- ["#Skylighting", "#KeywordTok"]
24+
---
25+
26+
With idiomatic highlighting, brand monospace-block properties apply directly
27+
to `raw.where(block: true)` which matches native Typst code blocks.
28+
This is the baseline that "just works."
29+
30+
Here's `x <- 1`{.r} with brand styling.
31+
32+
```python
33+
def hello():
34+
x = 1 + 2
35+
print(f"result: {x}")
36+
```
37+
38+
Both inline and block code should reflect brand styling.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
color:
2+
palette:
3+
mono-base-fg: "#2d3748"
4+
block-bg: "#edf2f7"
5+
inline-bg: "#fefcbf"
6+
7+
typography:
8+
fonts:
9+
- source: google
10+
family: Fira Code
11+
weight: [300, 400, 700]
12+
# Base monospace: family and weight inherited by both inline and block
13+
monospace:
14+
family: Fira Code
15+
weight: 400
16+
size: 0.85rem
17+
color: mono-base-fg
18+
# Block overrides only background-color; inherits family, weight, size, color
19+
monospace-block:
20+
background-color: block-bg
21+
line-height: 1.5
22+
# Inline overrides only background-color and weight; inherits family, size, color
23+
monospace-inline:
24+
background-color: inline-bg
25+
weight: 700

0 commit comments

Comments
 (0)