Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ All changes included in 1.9:
- ([#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.
- ([#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)
- ([#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.
- ([#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.

### `pdf`

Expand Down
74 changes: 74 additions & 0 deletions src/format/typst/format-typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { RenderServices } from "../../command/render/types.ts";
import { ProjectContext } from "../../project/types.ts";
import { BookExtension } from "../../project/types/book/book-shared.ts";
import {
kBrand,
kCiteproc,
kColumns,
kDefaultImageExtension,
kFigFormat,
kFigHeight,
kFigWidth,
kLight,
kLogo,
kNumberSections,
kSectionNumbering,
Expand All @@ -27,6 +29,7 @@ import {
Format,
FormatExtras,
FormatPandoc,
LightDarkBrand,
Metadata,
PandocFlags,
} from "../../config/types.ts";
Expand Down Expand Up @@ -142,15 +145,86 @@ export function typstFormat(): Format {
].map((partial) => join(templateDir, partial)),
};

// Postprocessor to fix Skylighting code block styling (issue #14126).
// Pandoc's generated Skylighting function uses block(fill: bgcolor, blocks)
// which lacks width, inset, and radius. We surgically fix this in the .typ
// output. If brand monospace-block has a background-color, we also override
// the bgcolor value.
const brandData = (format.render[kBrand] as LightDarkBrand | undefined)
?.[kLight];
const monospaceBlock = brandData?.processedData?.typography?.[
"monospace-block"
];
let brandBgColor = (monospaceBlock && typeof monospaceBlock !== "string")
? monospaceBlock["background-color"] as string | undefined
: undefined;
// Resolve palette color names (e.g. "code-bg" → "#1e1e2e")
if (brandBgColor && brandData?.data?.color?.palette) {
const palette = brandData.data.color.palette as Record<string, string>;
let resolved = brandBgColor;
while (palette[resolved]) {
resolved = palette[resolved];
}
brandBgColor = resolved;
}

return {
pandoc,
metadata,
templateContext,
postprocessors: [
skylightingPostProcessor(brandBgColor),
],
};
},
});
}

// Fix Skylighting code block styling in .typ output (issue #14126).
// The Pandoc-generated Skylighting function uses block(fill: bgcolor, blocks)
// which lacks width, inset, and radius. This postprocessor matches the entire
// Skylighting function by its distinctive signature and patches only within it.
// When brand provides a monospace-block background-color, also overrides the
// bgcolor value. This is a temporary workaround until the fix is upstreamed
// to the Skylighting library.
function skylightingPostProcessor(brandBgColor?: string) {
// Match the entire #let Skylighting(...) = { ... } function.
// The signature is stable and generated by Skylighting's Typst backend.
const skylightingFnRe =
/(#let Skylighting\(fill: none, number: false, start: 1, sourcelines\) = \{[\s\S]*?\n\})/;

return async (output: string) => {
const content = Deno.readTextFileSync(output);

const match = skylightingFnRe.exec(content);
if (!match) {
// No Skylighting function found — document may not have code blocks,
// or upstream changed the function signature. Nothing to patch.
return;
}

let fn = match[1];

// Fix block() call: add width, inset, radius
fn = fn.replace(
"block(fill: bgcolor, blocks)",
"block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks)",
);

// Override bgcolor with brand monospace-block background-color
if (brandBgColor) {
fn = fn.replace(
/let bgcolor = rgb\("[^"]*"\)/,
`let bgcolor = rgb("${brandBgColor}")`,
);
}

if (fn !== match[1]) {
Deno.writeTextFileSync(output, content.replace(match[1], fn));
}
};
}

function typstResolveFormat(format: Format) {
// Pandoc citeproc with typst output requires adjustment
// https://github.com/jgm/pandoc/commit/e89a3edf24a025d5bb0fe8c4c7a8e6e0208fa846
Expand Down
1 change: 1 addition & 0 deletions src/resources/filters/quarto-post/typst-brand-yaml.lua
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ function render_typst_brand_yaml()
}))
end
end

end,
Meta = function(meta)
local brand = param('brand')
Expand Down
2 changes: 1 addition & 1 deletion tests/docs/smoke-all/typst/code-listing-alignment.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ _quarto:
- subject: "CODESTART_func():"
relation: leftAligned
object: "BODYTEXT_ALIGN_MARKER"
tolerance: 5
tolerance: 10 # 8pt inset from Skylighting block styling
---

BODYTEXT_ALIGN_MARKER is body text that should left-align with code listings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ _quarto:
# Full-page listing code should be near left page edge (within body margin)
- subject: "docker"
relation: leftAligned
object: { role: "Page", page: 1, edge: "left" }
tolerance: 50 # Body margin ~47pt from page edge
object: { role: "Page", page: 2, edge: "left" }
tolerance: 58 # Body margin ~47pt + 8pt Skylighting inset from page edge
- # Negative: fullwidth listing caption NOT rightOf margin note
- subject: "FULLWIDTH-CSS-STYLES"
relation: rightOf
Expand Down
2 changes: 1 addition & 1 deletion tests/docs/smoke-all/typst/orange-book/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ _quarto:
- subject: "ALIGNTEST_MARKER"
relation: leftAligned
object: "LISTING_BODY_ALIGN_TEST"
tolerance: 5
tolerance: 10 # 8pt inset from Skylighting block styling
---

# Preface {.unnumbered}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
color:
palette:
code-fg: "#2d3748"

typography:
monospace-block:
color: code-fg
weight: 500
size: 10pt
line-height: 1.5
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: Brand Monospace Block without Background Color
format:
typst:
keep-typ: true
_quarto:
tests:
typst:
ensureTypstFileRegexMatches:
-
# Skylighting is active (default)
- "#Skylighting"
- "#KeywordTok"
# Brand monospace-block text properties emitted as show rules
- '^#show raw\.where\(block: true\): set text\(weight: 500, size: 10pt, fill: rgb\("#2d3748"\), \)$'
- '^#show raw\.where\(block: true\): set par\(leading: 0\.75em\)$'
# Even without brand bg, Skylighting override uses theme bgcolor
# so that width/inset/radius are applied
- 'let bgcolor = rgb\("#f1f3f5"\)'
- 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)'
# No brand background-color show rule (not configured)
- ['^#show raw\.where\(block: true\): set block\(fill:']
---

Brand sets monospace-block color, weight, size, and line-height but NOT
background-color. The Skylighting override should still be emitted using
the theme's background color so that code blocks get proper width/inset/radius.

```python
def hello():
x = 1 + 2
print(f"result: {x}")
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
color:
palette:
code-bg: "#1e1e2e"
code-fg: "#cdd6f4"

typography:
monospace-block:
color: code-fg
background-color: code-bg
size: 10pt
weight: 400
line-height: 1.6
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Brand Monospace Block with Skylighting
format:
typst:
keep-typ: true
_quarto:
tests:
typst:
ensureTypstFileRegexMatches:
-
# Skylighting is active (default)
- "#Skylighting"
- "#KeywordTok"
# Brand monospace-block properties are emitted as show rules
# (still useful for idiomatic mode fallback)
- '^#show raw\.where\(block: true\): set text\(weight: 400, size: 10pt, fill: rgb\("#cdd6f4"\), \)$'
- '^#show raw\.where\(block: true\): set block\(fill: rgb\("#1e1e2e"\)\)$'
- '^#show raw\.where\(block: true\): set par\(leading: 0\.85em\)$'
# Quarto-generated Skylighting override with brand bg and proper block styling
- 'let bgcolor = rgb\("#1e1e2e"\)'
- 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)'
# Should NOT have raw fenced blocks
- ["```python"]
---

Brand monospace-block options should apply to Skylighting code blocks.

```python
def hello():
x = 1 + 2
print(f"result: {x}")
```

Inline code like `hello()`{.python} should NOT get monospace-block styling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
color:
palette:
block-bg: "#f0f4f8"
block-fg: "#1a365d"
inline-bg: "#fed7d7"
inline-fg: "#9b2c2c"

typography:
monospace-block:
color: block-fg
background-color: block-bg
size: 11pt
weight: 400
line-height: 1.5
monospace-inline:
color: inline-fg
background-color: inline-bg
weight: 600
size: 0.9rem
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Brand Monospace with Idiomatic Highlighting
format:
typst:
keep-typ: true
syntax-highlighting: idiomatic
_quarto:
tests:
typst:
ensureTypstFileRegexMatches:
-
# Idiomatic = native typst highlighting = raw fenced code blocks
- "```python"
# Brand monospace-block properties (these target raw.where(block: true)
# which DOES match native/idiomatic code blocks)
- '^#show raw\.where\(block: true\): set text\(weight: 400, size: 11pt, fill: rgb\("#1a365d"\), \)$'
- '^#show raw\.where\(block: true\): set block\(fill: rgb\("#f0f4f8"\)\)$'
- '^#show raw\.where\(block: true\): set par\(leading: 0\.75em\)$'
# Brand monospace-inline properties
- '^#show raw\.where\(block: false\): set text\(weight: 600, size: 0\.9em, fill: rgb\("#9b2c2c"\), \)$'
- '^#show raw\.where\(block: false\): content => highlight\(fill: rgb\("#fed7d7"\), content\)$'
# Should NOT have Skylighting tokens
- ["#Skylighting", "#KeywordTok"]
---

With idiomatic highlighting, brand monospace-block properties apply directly
to `raw.where(block: true)` which matches native Typst code blocks.
This is the baseline that "just works."

Here's `x <- 1`{.r} with brand styling.

```python
def hello():
x = 1 + 2
print(f"result: {x}")
```

Both inline and block code should reflect brand styling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
color:
palette:
mono-base-fg: "#2d3748"
block-bg: "#edf2f7"
inline-bg: "#fefcbf"

typography:
fonts:
- source: google
family: Fira Code
weight: [300, 400, 700]
# Base monospace: family and weight inherited by both inline and block
monospace:
family: Fira Code
weight: 400
size: 0.85rem
color: mono-base-fg
# Block overrides only background-color; inherits family, weight, size, color
monospace-block:
background-color: block-bg
line-height: 1.5
# Inline overrides only background-color and weight; inherits family, size, color
monospace-inline:
background-color: inline-bg
weight: 700
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Brand Monospace Inheritance with Skylighting
format:
typst:
keep-typ: true
_quarto:
tests:
typst:
ensureTypstFileRegexMatches:
-
# Skylighting is active (default)
- "#Skylighting"
# Base monospace family applied via codefont
- 'codefont: \("Fira Code",\),$'
# monospace-block inherits color from base monospace, gets its own bg
- '^#show raw\.where\(block: true\): set text\(weight: 400, size: 0\.85em, fill: rgb\("#2d3748"\), \)$'
- '^#show raw\.where\(block: true\): set block\(fill: rgb\("#edf2f7"\)\)$'
- '^#show raw\.where\(block: true\): set par\(leading: 0\.75em\)$'
# Quarto Skylighting override with inherited brand bg
- 'let bgcolor = rgb\("#edf2f7"\)'
- 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)'
# monospace-inline overrides weight to 700, inherits color, gets its own bg
- '^#show raw\.where\(block: false\): set text\(weight: 700, size: 0\.85em, fill: rgb\("#2d3748"\), \)$'
- '^#show raw\.where\(block: false\): content => highlight\(fill: rgb\("#fefcbf"\), content\)$'
- ["```python"]
---

This tests that `monospace` base properties are properly inherited by
`monospace-block` and `monospace-inline`, with specific overrides taking
precedence.

Inline code: `hello()`{.python} and `x + y`{.r} should use bold weight (700) from
monospace-inline override, with yellow background.

```python
# Block code inherits base weight (400) with blue-gray background
def greet(name):
return f"Hello, {name}!"
```

More `len(x)`{.python} code references.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
color:
palette:
inline-bg: "#fff3cd"
inline-fg: "#664d03"

typography:
monospace-inline:
color: inline-fg
background-color: inline-bg
weight: 600
size: 0.85rem
Loading
Loading