Skip to content

Commit e0e9848

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. For brand documents, also resolves monospace-block background-color (including palette color names) and overrides the bgcolor value. Fixes #14126
1 parent 3f3f341 commit e0e9848

17 files changed

Lines changed: 442 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: 65 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,77 @@ 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 surgically replaces
186+
// that call. When brand provides a monospace-block background-color, also
187+
// overrides the bgcolor value. This is a temporary workaround until the fix
188+
// is upstreamed to the Skylighting library.
189+
function skylightingPostProcessor(brandBgColor?: string) {
190+
return async (output: string) => {
191+
let content = Deno.readTextFileSync(output);
192+
let modified = false;
193+
194+
// Fix block() call: add width, inset, radius
195+
const replaced = content.replaceAll(
196+
"block(fill: bgcolor, blocks)",
197+
"block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks)",
198+
);
199+
if (replaced !== content) {
200+
content = replaced;
201+
modified = true;
202+
}
203+
204+
// Override bgcolor with brand monospace-block background-color
205+
if (brandBgColor) {
206+
content = content.replaceAll(
207+
/let bgcolor = rgb\("[^"]*"\)/g,
208+
`let bgcolor = rgb("${brandBgColor}")`,
209+
);
210+
modified = true;
211+
}
212+
213+
if (modified) {
214+
Deno.writeTextFileSync(output, content);
215+
}
216+
};
217+
}
218+
154219
function typstResolveFormat(format: Format) {
155220
// Pandoc citeproc with typst output requires adjustment
156221
// 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()` 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 `inline code` 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)