Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d607d11
feat: add unified quarto-code-block wrapper to Typst definitions
mcanouil Mar 6, 2026
b3e04ce
feat: pass syntax-highlighting boolean to Typst filter params
mcanouil Mar 6, 2026
e50c0bd
feat: add Typst code annotation processing to Lua filter
mcanouil Mar 6, 2026
6ce0416
feat: extend skylightingPostProcessor for code annotations
mcanouil Mar 6, 2026
80b2fcf
feat: add Typst renderer for DecoratedCodeBlock filename bar
mcanouil Mar 6, 2026
651075e
test: add Typst code annotation and filename bar test documents
mcanouil Mar 6, 2026
b5c6da8
refactor: split quarto-code-block into quarto-code-filename and quart…
mcanouil Mar 6, 2026
9e800ff
test: expand Typst code annotation and filename test coverage
mcanouil Mar 6, 2026
8241543
fix: harden Typst code annotation and filename escaping
mcanouil Mar 6, 2026
53f0ede
Merge branch 'main' into feat/typst-annotation-filename
mcanouil Mar 6, 2026
9416ad1
test: tweak test files
mcanouil Mar 6, 2026
78a54f8
fix: merge parent block from code cell with annotation marker regex f…
mcanouil Mar 6, 2026
c0f1448
fix: improve semantic structure by linking code and annotation
mcanouil Mar 7, 2026
ac932e9
test: update tests with semantic links
mcanouil Mar 7, 2026
5c8b344
fix: ensure back-labels are emitted only once
mcanouil Mar 7, 2026
994a878
test: update block styling to include stroke in monospace tests
mcanouil Mar 7, 2026
6efcd12
test: update block styling to include stroke in brand monospace block
mcanouil Mar 8, 2026
da9fb87
fix: escape newline, carriage return, and tab characters in filename
mcanouil Mar 9, 2026
f9a5d50
Merge branch 'main' into feat/typst-annotation-filename
mcanouil Mar 9, 2026
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
11 changes: 11 additions & 0 deletions src/command/render/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ import {
kResourcePath,
kShortcodes,
kTblColwidths,
kHighlightStyle,
kSyntaxHighlighting,
kTocTitleDocument,
kUnrollMarkdownCells,
kUseRsvgConvert,
} from "../../config/constants.ts";
import { kDefaultHighlightStyle } from "./constants.ts";
import { PandocOptions } from "./types.ts";
import {
Format,
Expand Down Expand Up @@ -945,11 +948,19 @@ async function resolveFilterExtension(
}

const extractTypstFilterParams = (format: Format) => {
const theme =
format.pandoc[kSyntaxHighlighting] ||
format.pandoc[kHighlightStyle] ||
kDefaultHighlightStyle;
const skylighting =
typeof theme === "string" && theme !== "none" && theme !== "idiomatic";

return {
[kTocIndent]: format.metadata[kTocIndent],
[kLogo]: format.metadata[kLogo],
[kCssPropertyProcessing]: format.metadata[kCssPropertyProcessing],
[kBrandMode]: format.metadata[kBrandMode],
[kHtmlPreTagProcessing]: format.metadata[kHtmlPreTagProcessing],
[kSyntaxHighlighting]: skylighting,
};
};
106 changes: 88 additions & 18 deletions src/format/typst/format-typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,40 +187,110 @@ export function typstFormat(): Format {
// 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.
//
// Additionally patches the Skylighting function for code annotation support:
// adds an annotations parameter, moves line tracking outside the if-number
// block, adds per-line annotation rendering, and routes output through
// quarto-code-block(). Also merges annotation comment markers from the Lua
// filter into Skylighting call sites.
//
// Upstream compatibility: a PR to skylighting-format-typst
// (fix/typst-skylighting-block-style) adds block styling upstream. Once merged
// and picked up by Pandoc, the block styling patch becomes a no-op (the
// replace target won't match). The brand color regex targets rgb("...") which
// works with both current and future upstream bgcolor init patterns.
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\})/;

// Annotation markers emitted by the Lua filter as Typst comments
const annotationMarkerRe =
/\/\/ quarto-code-annotations: ([\w-]*) (\([^)]*\))\n(\s*(?:#block\[\s*)*(?:#quarto-code-filename\([^\n]*\)\[\s*)?)#Skylighting\(/g;

return async (output: string) => {
const content = Deno.readTextFileSync(output);
let content = Deno.readTextFileSync(output).replace(/\r\n/g, "\n");
let changed = false;

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;
}
if (match) {
let fn = match[1];

let fn = match[1];
// Fix block() call: add width, inset, radius, stroke
fn = fn.replace(
"block(fill: bgcolor, blocks)",
"block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0.5pt + luma(200), blocks)",
);

// 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(
/rgb\("[^"]*"\)/,
`rgb("${brandBgColor}")`,
);
}

// Add cell-id and annotations parameters to function signature
fn = fn.replace(
"start: 1, sourcelines)",
"start: 1, cell-id: \"\", annotations: (:), sourcelines)",
);

// Move lnum increment outside if-number block (always track position)
fn = fn.replace(
/if number \{\n\s+lnum = lnum \+ 1\n/,
"lnum = lnum + 1\n if number {\n",
);

// Initialise a dictionary to track which annotation numbers have
// already emitted a back-label (avoids duplicate labels when one
// annotation spans multiple lines).
fn = fn.replace(
/let lnum = start - 1\n/,
"let lnum = start - 1\n let seen-annotes = (:)\n",
);

// Override bgcolor with brand monospace-block background-color
if (brandBgColor) {
// Add annotation rendering per line (derive circle colour from bgcolor)
fn = fn.replace(
/let bgcolor = rgb\("[^"]*"\)/,
`let bgcolor = rgb("${brandBgColor}")`,
"blocks = blocks + ln + EndLine()",
`let annote-num = annotations.at(str(lnum), default: none)
if annote-num != none {
if cell-id != "" {
let lbl = cell-id + "-annote-" + str(annote-num)
if str(annote-num) not in seen-annotes {
seen-annotes.insert(str(annote-num), true)
blocks = blocks + box(width: 100%)[#ln #h(1fr) #link(label(lbl))[#quarto-circled-number(annote-num, color: quarto-annote-color(bgcolor))] #label(lbl + "-back")] + EndLine()
} else {
blocks = blocks + box(width: 100%)[#ln #h(1fr) #link(label(lbl))[#quarto-circled-number(annote-num, color: quarto-annote-color(bgcolor))]] + EndLine()
}
} else {
blocks = blocks + box(width: 100%)[#ln #h(1fr) #quarto-circled-number(annote-num, color: quarto-annote-color(bgcolor))] + EndLine()
}
} else {
blocks = blocks + ln + EndLine()
}`,
);

if (fn !== match[1]) {
content = content.replace(match[1], fn);
changed = true;
}
}

// Merge annotation markers into Skylighting call sites, including
// optional #block[ wrappers and #quarto-code-filename(...)[ wrappers.
const merged = content.replace(
annotationMarkerRe,
"$3#Skylighting(cell-id: \"$1\", annotations: $2, ",
);
if (merged !== content) {
content = merged;
changed = true;
}

if (fn !== match[1]) {
Deno.writeTextFileSync(output, content.replace(match[1], fn));
if (changed) {
Deno.writeTextFileSync(output, content);
}
};
}
Expand Down
29 changes: 29 additions & 0 deletions src/resources/filters/customnodes/decoratedcodeblock.lua
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,32 @@ _quarto.ast.add_renderer("DecoratedCodeBlock",

return pandoc.Div(blocks, pandoc.Attr("", classes))
end)

-- typst renderer
_quarto.ast.add_renderer("DecoratedCodeBlock",
function(_)
return _quarto.format.isTypstOutput()
end,
function(node)
if node.filename == nil then
return _quarto.ast.walk(quarto.utils.as_blocks(node.code_block), {
CodeBlock = render_folded_block
})
end
local el = node.code_block
local rendered = _quarto.ast.walk(quarto.utils.as_blocks(el), {
CodeBlock = render_folded_block
}) or pandoc.Blocks({})
local blocks = pandoc.Blocks({})
local escaped = node.filename
:gsub('\\', '\\\\')
:gsub('"', '\\"')
:gsub('\n', '\\n')
:gsub('\r', '\\r')
:gsub('\t', '\\t')
blocks:insert(pandoc.RawBlock("typst",
'#quarto-code-filename("' .. escaped .. '")['))
blocks:extend(rendered)
blocks:insert(pandoc.RawBlock("typst", "]"))
return pandoc.Div(blocks)
end)
2 changes: 2 additions & 0 deletions src/resources/filters/modules/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ local kAsciidocNativeCites = 'use-asciidoc-native-cites'
local kShowNotes = 'showNotes'
local kProjectResolverIgnore = 'project-resolve-ignore'

local kSyntaxHighlighting = 'syntax-highlighting'
local kCodeAnnotationsParam = 'code-annotations'
local kDataCodeCellTarget = 'data-code-cell'
local kDataCodeCellLines = 'data-code-lines'
Expand Down Expand Up @@ -200,6 +201,7 @@ return {
kAsciidocNativeCites = kAsciidocNativeCites,
kShowNotes = kShowNotes,
kProjectResolverIgnore = kProjectResolverIgnore,
kSyntaxHighlighting = kSyntaxHighlighting,
kCodeAnnotationsParam = kCodeAnnotationsParam,
kDataCodeCellTarget = kDataCodeCellTarget,
kDataCodeCellLines = kDataCodeCellLines,
Expand Down
Loading
Loading