From d607d11fd9116b3f9c50e18c8c449cb6dd2c99e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:15:49 +0100 Subject: [PATCH 01/15] feat: add unified quarto-code-block wrapper to Typst definitions Add quarto-circled-number, quarto-code-block, and quarto-annotation-item functions for code annotation support and filename bar in Typst output. Route all native raw code blocks through the unified wrapper. --- .../typst/pandoc/quarto/definitions.typ | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index 40dc1a164b..b3f1fcba36 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -24,12 +24,48 @@ // Some quarto-specific definitions. -#show raw.where(block: true): set block( - fill: luma(230), - width: 100%, - inset: 8pt, - radius: 2pt - ) +// Code annotation support +#let quarto-circled-number(n) = { + box(baseline: 15%, circle( + radius: 0.55em, + stroke: 0.5pt + text.fill, + )[#set text(size: 0.7em); #align(center + horizon, str(n))]) +} + +#let quarto-code-block(fill: luma(230), filename: none, annotations: (:), body) = { + // Prevent the outer show rule from re-wrapping inner raw blocks + show raw.where(block: true): it => it + // Handle annotations for native raw blocks (no-op when annotations is empty) + show raw.line: it => { + let annote-num = annotations.at(str(it.number), default: none) + if annote-num != none { + box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num)] + } else { + it + } + } + block(fill: fill, width: 100%, inset: 0pt, radius: 2pt, clip: true)[ + #if filename != none { + block( + fill: fill.darken(10%), + width: 100%, + inset: (x: 8pt, y: 4pt), + )[#text(size: 0.85em, weight: "bold")[#filename]] + } + #block(inset: 8pt, width: 100%, body) + ] +} + +#let quarto-annotation-item(n, content) = { + block(above: 0.4em, below: 0.4em)[ + #quarto-circled-number(n) + #h(0.4em) + #content + ] +} + +// Route all native raw code blocks through the unified wrapper +#show raw.where(block: true): it => quarto-code-block(it) #let block_with_new_content(old_block, new_content) = { let fields = old_block.fields() From b3e04ce9d3c0b4dbbadfebba0772e9f3bede75db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:21:09 +0100 Subject: [PATCH 02/15] feat: pass syntax-highlighting boolean to Typst filter params Lua filters need to know whether Skylighting is active to choose between emitting annotation markers (Skylighting) or wrapping CodeBlocks directly (native/none highlighting). --- src/command/render/filters.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/command/render/filters.ts b/src/command/render/filters.ts index 863d3d9603..ff2cfe685a 100644 --- a/src/command/render/filters.ts +++ b/src/command/render/filters.ts @@ -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, @@ -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, }; }; From e50c0bd363ab7124ba4fdc6a70a44abe3a3cc89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:27:00 +0100 Subject: [PATCH 03/15] feat: add Typst code annotation processing to Lua filter Add helper functions for Typst annotation data (typstAnnotationsDict, typstAnnotationMarker, wrapTypstAnnotatedCode), register Typst annotation processor, and handle standalone CodeBlock, DecoratedCodeBlock, and OL paths for Typst output. Skylighting mode emits a comment marker for the TS post-processor. Native mode wraps CodeBlocks in quarto-code-block with annotations. OL items become quarto-annotation-item raw Typst blocks. --- src/resources/filters/modules/constants.lua | 2 + .../filters/quarto-pre/code-annotation.lua | 156 +++++++++++++++++- 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/src/resources/filters/modules/constants.lua b/src/resources/filters/modules/constants.lua index cb22ee3be6..0fd8d29215 100644 --- a/src/resources/filters/modules/constants.lua +++ b/src/resources/filters/modules/constants.lua @@ -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' @@ -200,6 +201,7 @@ return { kAsciidocNativeCites = kAsciidocNativeCites, kShowNotes = kShowNotes, kProjectResolverIgnore = kProjectResolverIgnore, + kSyntaxHighlighting = kSyntaxHighlighting, kCodeAnnotationsParam = kCodeAnnotationsParam, kDataCodeCellTarget = kDataCodeCellTarget, kDataCodeCellLines = kDataCodeCellLines, diff --git a/src/resources/filters/quarto-pre/code-annotation.lua b/src/resources/filters/quarto-pre/code-annotation.lua index e09e9f036f..e472d219c3 100644 --- a/src/resources/filters/quarto-pre/code-annotation.lua +++ b/src/resources/filters/quarto-pre/code-annotation.lua @@ -81,7 +81,60 @@ local function toAnnoteId(number) end local function latexListPlaceholder(number) - return '5CB6E08D-list-annote-' .. number + return '5CB6E08D-list-annote-' .. number +end + +-- Typst annotation helpers + +-- Convert annotations table to a flat Typst dictionary string. +-- Keys are line positions (as strings), values are annotation numbers. +local function typstAnnotationsDict(annotations) + local entries = {} + for annoteId, lineNumbers in pairs(annotations) do + local num = annoteId:match("annote%-(%d+)") + if num then + for _, lineNo in ipairs(lineNumbers) do + table.insert(entries, {pos = lineNo, annoteNum = tonumber(num)}) + end + end + end + table.sort(entries, function(a, b) return a.pos < b.pos end) + local parts = {} + for _, e in ipairs(entries) do + table.insert(parts, '"' .. tostring(e.pos) .. '": ' .. tostring(e.annoteNum)) + end + return '(' .. table.concat(parts, ', ') .. ')' +end + +-- Skylighting mode: emit a Typst comment that the TS post-processor +-- will merge into the Skylighting call site. +local function typstAnnotationMarker(annotations) + local dict = typstAnnotationsDict(annotations) + return pandoc.RawBlock("typst", "// quarto-code-annotations: " .. dict) +end + +-- Native/none mode: wrap a CodeBlock in #quarto-code-block(annotations: ...). +-- raw.line numbers always start at 1 regardless of startFrom, so adjust keys. +local function wrapTypstAnnotatedCode(codeBlock, annotations) + local startFrom = tonumber(codeBlock.attr.attributes['startFrom']) or 1 + local adjustedAnnotations = {} + for annoteId, lineNumbers in pairs(annotations) do + local adjusted = pandoc.List({}) + for _, lineNo in ipairs(lineNumbers) do + adjusted:insert(lineNo - startFrom + 1) + end + adjustedAnnotations[annoteId] = adjusted + end + local dict = typstAnnotationsDict(adjustedAnnotations) + local lang = codeBlock.attr.classes[1] or "" + local code = codeBlock.text + local maxBackticks = 2 + for seq in code:gmatch("`+") do + maxBackticks = math.max(maxBackticks, #seq) + end + local fence = string.rep("`", maxBackticks + 1) + local raw = "#quarto-code-block(annotations: " .. dict .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" + return pandoc.RawBlock("typst", raw) end local function toLines(s) @@ -263,11 +316,15 @@ end function processAnnotation(line, annoteNumber, annotationProvider) -- For all other formats, just strip the annotation- the definition list is converted - -- to be based upon line numbers. + -- to be based upon line numbers. local stripped = annotationProvider.stripAnnotation(line, annoteNumber) return stripped end +function processTypstAnnotation(line, annoteNumber, annotationProvider) + return annotationProvider.stripAnnotation(line, annoteNumber) +end + function code_meta() return { Meta = function(meta) @@ -361,6 +418,8 @@ function code_annotations() annotationProcessor = processLaTeXAnnotation elseif _quarto.format.isAsciiDocOutput() then annotationProcessor = processAsciidocAnnotation + elseif _quarto.format.isTypstOutput() then + annotationProcessor = processTypstAnnotation end -- resolve annotations @@ -427,14 +486,26 @@ function code_annotations() if #block.content == 1 and #block.content[1].content == 1 then -- Find the code block and process that local codeblock = block.content[1].content[1] - + local cellId = resolveCellId(codeblock.attr.identifier) local codeCell = processCodeCell(codeblock, cellId) if codeCell then if codeAnnotations ~= constants.kCodeAnnotationStyleNone then codeCell.attr.identifier = cellId; end - block.content[1].content[1] = codeCell + -- Typst DecoratedCodeBlock: emit annotation data + if _quarto.format.isTypstOutput() + and codeAnnotations ~= constants.kCodeAnnotationStyleNone + and pendingAnnotations and next(pendingAnnotations) ~= nil then + if param(constants.kSyntaxHighlighting, true) then + outputBlock(typstAnnotationMarker(pendingAnnotations)) + block.content[1].content[1] = codeCell + else + block.content[1].content[1] = wrapTypstAnnotatedCode(codeCell, pendingAnnotations) + end + else + block.content[1].content[1] = codeCell + end outputBlock(block) else outputBlockClearPending(block) @@ -460,7 +531,20 @@ function code_annotations() if codeAnnotations ~= constants.kCodeAnnotationStyleNone then codeCell.attr.identifier = cellId; end - outputBlock(codeCell) + -- Typst standalone CodeBlock: emit annotation data alongside + -- the code block. The OL handler will emit annotation items. + if _quarto.format.isTypstOutput() + and codeAnnotations ~= constants.kCodeAnnotationStyleNone + and pendingAnnotations and next(pendingAnnotations) ~= nil then + if param(constants.kSyntaxHighlighting, true) then + outputBlock(typstAnnotationMarker(pendingAnnotations)) + outputBlock(codeCell) + else + outputBlock(wrapTypstAnnotatedCode(codeCell, pendingAnnotations)) + end + else + outputBlock(codeCell) + end else outputBlockClearPending(block) end @@ -468,6 +552,59 @@ function code_annotations() outputBlockClearPending(block) end elseif block.t == 'OrderedList' and pendingAnnotations ~= nil and next(pendingAnnotations) ~= nil then + + -- Typst: emit annotation items as raw Typst blocks + if _quarto.format.isTypstOutput() and codeAnnotations ~= constants.kCodeAnnotationStyleNone then + local annotationBlocks = pandoc.List() + for i, v in ipairs(block.content) do + local annotationNumber = block.start + i - 1 + local annoteId = toAnnoteId(annotationNumber) + if pendingAnnotations[annoteId] then + local content = pandoc.utils.stringify(v[1]) + annotationBlocks:insert(pandoc.RawBlock("typst", + "#quarto-annotation-item(" .. tostring(annotationNumber) .. ", [" .. content .. "])")) + end + end + + local useSkylighting = param(constants.kSyntaxHighlighting, true) + + if pendingCodeCell ~= nil then + local resolvedCell = _quarto.ast.walk(pendingCodeCell, { + CodeBlock = function(el) + if el.attr.classes:find('cell-code') or + el.attr.classes:find(constants.kDataCodeAnnonationClz) then + if useSkylighting then + return nil + else + return wrapTypstAnnotatedCode(el, pendingAnnotations) + end + end + end + }) + + if useSkylighting then + outputBlock(typstAnnotationMarker(pendingAnnotations)) + end + + local dlDiv = pandoc.Div(annotationBlocks, pandoc.Attr("", {constants.kCellAnnotationClass})) + if is_custom_node(resolvedCell) then + local custom = _quarto.ast.resolve_custom_data(resolvedCell) or pandoc.Div({}) + custom.content:insert(2, dlDiv) + else + resolvedCell.content:insert(2, dlDiv) + end + outputBlock(resolvedCell) + else + for _, ab in ipairs(annotationBlocks) do + outputBlock(ab) + end + end + pendingAnnotations = nil + pendingCellId = nil + pendingCodeCell = nil + + -- Generic handler for all other formats + else -- There are pending annotations, which means this OL is immediately after -- a code cell with annotations. Use to emit a DL describing the code local items = pandoc.List() @@ -496,7 +633,7 @@ function code_annotations() end -- compute the definition for the DD - local definitionContent = v[1].content + local definitionContent = v[1].content local annotationToken = tostring(annotationNumber); -- Only output span for certain formats (HTML) @@ -511,7 +648,7 @@ function code_annotations() {constants.kDataCodeCellAnnotation, annotationToken} } definition = pandoc.Span(definitionContent, pandoc.Attr(attribs)) - else + else definition = pandoc.Plain(definitionContent) end @@ -554,15 +691,16 @@ function code_annotations() flushPending() else if requireNonIncremental then - -- wrap in Non Incremental Div to prevent automatique + -- wrap in Non Incremental Div to prevent automatique outputBlockClearPending(pandoc.Div({dl}, pandoc.Attr("", {constants.kNonIncremental}))) - else + else outputBlockClearPending(dl) end end else flushPending() end + end -- end generic handler else outputBlockClearPending(block) end From 6ce0416f24797cb2643ab53b34ec35c0665c0d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:29:52 +0100 Subject: [PATCH 04/15] feat: extend skylightingPostProcessor for code annotations Patch Skylighting function to accept annotations parameter, track line position unconditionally, render circled annotation numbers per line, and route output through quarto-code-block wrapper. Merge Lua-emitted annotation comment markers into Skylighting call sites. --- src/format/typst/format-typst.ts | 93 +++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index 71f0657022..d4c8f00bea 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -187,40 +187,97 @@ 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: (\([^)]*\))\n\s*#Skylighting\(/g; + return async (output: string) => { - const content = Deno.readTextFileSync(output); + let content = Deno.readTextFileSync(output); + 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 + fn = fn.replace( + "block(fill: bgcolor, blocks)", + "block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, 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 annotations parameter to function signature + fn = fn.replace( + "start: 1, sourcelines)", + "start: 1, 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", + ); + + // Add annotation rendering per line + fn = fn.replace( + "blocks = blocks + ln + EndLine()", + `let annote-num = annotations.at(str(lnum), default: none) + if annote-num != none { + blocks = blocks + box(width: 100%)[#ln #h(1fr) #quarto-circled-number(annote-num)] + EndLine() + } else { + blocks = blocks + ln + EndLine() + }`, + ); - // Override bgcolor with brand monospace-block background-color - if (brandBgColor) { + // Route through unified quarto-code-block wrapper fn = fn.replace( - /let bgcolor = rgb\("[^"]*"\)/, - `let bgcolor = rgb("${brandBgColor}")`, + /block\(fill: bgcolor,[^)]*blocks\)/, + "quarto-code-block(fill: bgcolor, blocks)", ); + + if (fn !== match[1]) { + content = content.replace(match[1], fn); + changed = true; + } + } + + // Merge annotation markers into Skylighting call sites + const merged = content.replace( + annotationMarkerRe, + "#Skylighting(annotations: $1, (", + ); + 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); } }; } From 80b2fcfa253dbc665ecb42156d30292fc5ac12a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:30:42 +0100 Subject: [PATCH 05/15] feat: add Typst renderer for DecoratedCodeBlock filename bar Wrap code blocks with filename attribute in quarto-code-block(filename: ...) for Typst output, rendering a simple filename bar above the code. --- .../customnodes/decoratedcodeblock.lua | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/resources/filters/customnodes/decoratedcodeblock.lua b/src/resources/filters/customnodes/decoratedcodeblock.lua index 5aeefb0a55..7de4513ed1 100644 --- a/src/resources/filters/customnodes/decoratedcodeblock.lua +++ b/src/resources/filters/customnodes/decoratedcodeblock.lua @@ -180,3 +180,26 @@ _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({}) + blocks:insert(pandoc.RawBlock("typst", + '#quarto-code-block(filename: "' .. node.filename .. '")[')) + blocks:extend(rendered) + blocks:insert(pandoc.RawBlock("typst", "]")) + return pandoc.Div(blocks) + end) From 651075ed4fd5551058628a14ac994f60ba9a4caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:36:30 +0100 Subject: [PATCH 06/15] test: add Typst code annotation and filename bar test documents Add smoke-all tests for: - Code annotations with Skylighting (default) - Code annotations with native highlighting - Code annotations disabled (none) - Filename bar on code blocks --- .../code-annotations-native.qmd | 25 +++++++++++++++++++ .../code-annotations-none.qmd | 24 ++++++++++++++++++ .../code-annotations/code-annotations.qmd | 25 +++++++++++++++++++ .../typst/code-filename/code-filename.qmd | 18 +++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd create mode 100644 tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd create mode 100644 tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename.qmd diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd new file mode 100644 index 0000000000..bc2a59e174 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd @@ -0,0 +1,25 @@ +--- +title: Code Annotations (Native) +format: + typst: + keep-typ: true + code-annotations: true + syntax-highlighting: idiomatic +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - "quarto-code-block" + - "quarto-annotation-item" + - [] +--- + +```python +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd new file mode 100644 index 0000000000..eb28056cad --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd @@ -0,0 +1,24 @@ +--- +title: Code Annotations (None) +format: + typst: + keep-typ: true + code-annotations: none +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - [] + - + - "quarto-annotation-item" + - "quarto-circled-number" +--- + +```python +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd new file mode 100644 index 0000000000..d8ff469d81 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd @@ -0,0 +1,25 @@ +--- +title: Code Annotations (Skylighting) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - "quarto-circled-number" + - "quarto-annotation-item" + - "annotations:" + - [] +--- + +```python +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd new file mode 100644 index 0000000000..bef4754ca4 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd @@ -0,0 +1,18 @@ +--- +title: Code Filename Bar +format: + typst: + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-block\(filename:' + - [] +--- + +```{.python filename="example.py"} +def hello(): + print("Hello, world!") +``` From b5c6da87fab2ca83d6b3a363448c5d7e5ee82597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:36:11 +0100 Subject: [PATCH 07/15] refactor: split quarto-code-block into quarto-code-filename and quarto-code-annotation --- src/format/typst/format-typst.ts | 16 ++---- .../customnodes/decoratedcodeblock.lua | 2 +- .../filters/quarto-pre/code-annotation.lua | 30 ++++++----- .../typst/pandoc/quarto/definitions.typ | 54 ++++++++++++------- .../code-annotations-native.qmd | 2 +- .../typst/code-filename/code-filename.qmd | 2 +- 6 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index d4c8f00bea..40abe93e04 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -217,10 +217,10 @@ function skylightingPostProcessor(brandBgColor?: string) { if (match) { let fn = match[1]; - // Fix block() call: add width, inset, radius + // Fix block() call: add width, inset, radius, stroke fn = fn.replace( "block(fill: bgcolor, blocks)", - "block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks)", + "block(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0.5pt + luma(200), blocks)", ); // Override bgcolor with brand monospace-block background-color @@ -243,23 +243,17 @@ function skylightingPostProcessor(brandBgColor?: string) { "lnum = lnum + 1\n if number {\n", ); - // Add annotation rendering per line + // Add annotation rendering per line (derive circle colour from bgcolor) fn = fn.replace( "blocks = blocks + ln + EndLine()", `let annote-num = annotations.at(str(lnum), default: none) if annote-num != none { - blocks = blocks + box(width: 100%)[#ln #h(1fr) #quarto-circled-number(annote-num)] + EndLine() + blocks = blocks + box(width: 100%)[#ln #h(1fr) #quarto-circled-number(annote-num, color: quarto-annote-color(bgcolor))] + EndLine() } else { blocks = blocks + ln + EndLine() }`, ); - // Route through unified quarto-code-block wrapper - fn = fn.replace( - /block\(fill: bgcolor,[^)]*blocks\)/, - "quarto-code-block(fill: bgcolor, blocks)", - ); - if (fn !== match[1]) { content = content.replace(match[1], fn); changed = true; @@ -269,7 +263,7 @@ function skylightingPostProcessor(brandBgColor?: string) { // Merge annotation markers into Skylighting call sites const merged = content.replace( annotationMarkerRe, - "#Skylighting(annotations: $1, (", + "#Skylighting(annotations: $1, ", ); if (merged !== content) { content = merged; diff --git a/src/resources/filters/customnodes/decoratedcodeblock.lua b/src/resources/filters/customnodes/decoratedcodeblock.lua index 7de4513ed1..f355f37834 100644 --- a/src/resources/filters/customnodes/decoratedcodeblock.lua +++ b/src/resources/filters/customnodes/decoratedcodeblock.lua @@ -198,7 +198,7 @@ _quarto.ast.add_renderer("DecoratedCodeBlock", }) or pandoc.Blocks({}) local blocks = pandoc.Blocks({}) blocks:insert(pandoc.RawBlock("typst", - '#quarto-code-block(filename: "' .. node.filename .. '")[')) + '#quarto-code-filename("' .. node.filename .. '")[')) blocks:extend(rendered) blocks:insert(pandoc.RawBlock("typst", "]")) return pandoc.Div(blocks) diff --git a/src/resources/filters/quarto-pre/code-annotation.lua b/src/resources/filters/quarto-pre/code-annotation.lua index e472d219c3..19a41adecc 100644 --- a/src/resources/filters/quarto-pre/code-annotation.lua +++ b/src/resources/filters/quarto-pre/code-annotation.lua @@ -113,7 +113,7 @@ local function typstAnnotationMarker(annotations) return pandoc.RawBlock("typst", "// quarto-code-annotations: " .. dict) end --- Native/none mode: wrap a CodeBlock in #quarto-code-block(annotations: ...). +-- Native/none mode: wrap a CodeBlock in #quarto-code-annotation(annotations)[...]. -- raw.line numbers always start at 1 regardless of startFrom, so adjust keys. local function wrapTypstAnnotatedCode(codeBlock, annotations) local startFrom = tonumber(codeBlock.attr.attributes['startFrom']) or 1 @@ -133,7 +133,7 @@ local function wrapTypstAnnotatedCode(codeBlock, annotations) maxBackticks = math.max(maxBackticks, #seq) end local fence = string.rep("`", maxBackticks + 1) - local raw = "#quarto-code-block(annotations: " .. dict .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" + local raw = "#quarto-code-annotation(" .. dict .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" return pandoc.RawBlock("typst", raw) end @@ -322,7 +322,8 @@ function processAnnotation(line, annoteNumber, annotationProvider) end function processTypstAnnotation(line, annoteNumber, annotationProvider) - return annotationProvider.stripAnnotation(line, annoteNumber) + local stripped = annotationProvider.stripAnnotation(line, annoteNumber) + return stripped end function code_meta() @@ -483,7 +484,9 @@ function code_annotations() -- output the pending code cell and continue flushPending() - if #block.content == 1 and #block.content[1].content == 1 then + if #block.content == 1 and #block.content[1].content == 1 + and block.content[1].content[1] ~= nil + and block.content[1].content[1].t == "CodeBlock" then -- Find the code block and process that local codeblock = block.content[1].content[1] @@ -493,13 +496,14 @@ function code_annotations() if codeAnnotations ~= constants.kCodeAnnotationStyleNone then codeCell.attr.identifier = cellId; end - -- Typst DecoratedCodeBlock: emit annotation data + -- Typst DecoratedCodeBlock: embed annotation data inside the block + -- so the marker stays adjacent to the Skylighting call after rendering if _quarto.format.isTypstOutput() and codeAnnotations ~= constants.kCodeAnnotationStyleNone and pendingAnnotations and next(pendingAnnotations) ~= nil then if param(constants.kSyntaxHighlighting, true) then - outputBlock(typstAnnotationMarker(pendingAnnotations)) block.content[1].content[1] = codeCell + block.content[1].content:insert(1, typstAnnotationMarker(pendingAnnotations)) else block.content[1].content[1] = wrapTypstAnnotatedCode(codeCell, pendingAnnotations) end @@ -582,16 +586,18 @@ function code_annotations() end }) - if useSkylighting then - outputBlock(typstAnnotationMarker(pendingAnnotations)) - end - local dlDiv = pandoc.Div(annotationBlocks, pandoc.Attr("", {constants.kCellAnnotationClass})) if is_custom_node(resolvedCell) then local custom = _quarto.ast.resolve_custom_data(resolvedCell) or pandoc.Div({}) - custom.content:insert(2, dlDiv) + if useSkylighting then + custom.content:insert(1, typstAnnotationMarker(pendingAnnotations)) + end + custom.content:insert(dlDiv) else - resolvedCell.content:insert(2, dlDiv) + if useSkylighting then + resolvedCell.content:insert(1, typstAnnotationMarker(pendingAnnotations)) + end + resolvedCell.content:insert(dlDiv) end outputBlock(resolvedCell) else diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index b3f1fcba36..cefb9453a8 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -25,35 +25,48 @@ // Some quarto-specific definitions. // Code annotation support -#let quarto-circled-number(n) = { +#let quarto-circled-number(n, color: none) = context { + let c = if color != none { color } else { text.fill } box(baseline: 15%, circle( radius: 0.55em, - stroke: 0.5pt + text.fill, - )[#set text(size: 0.7em); #align(center + horizon, str(n))]) + stroke: 0.5pt + c, + )[#set text(size: 0.7em, fill: c); #align(center + horizon, str(n))]) } -#let quarto-code-block(fill: luma(230), filename: none, annotations: (:), body) = { - // Prevent the outer show rule from re-wrapping inner raw blocks +// Derive a contrasting annotation colour from a background fill. +// Light backgrounds get dark circles; dark backgrounds get light circles. +// Uses relative luminance: 0.2126R + 0.7152G + 0.0722B. +#let quarto-annote-color(bg) = { + if type(bg) == color { + let (r, g, b, ..) = bg.components(alpha: false) + let lum = 0.2126 * r / 100% + 0.7152 * g / 100% + 0.0722 * b / 100% + if lum < 0.5 { luma(200) } else { luma(60) } + } else { + luma(60) + } +} + +#let quarto-code-filename(filename, body) = { + show raw.where(block: true): it => it + block(width: 100%, radius: 2pt, clip: true, stroke: 0.5pt + luma(200))[ + #set block(spacing: 0pt) + #block(fill: luma(220), width: 100%, inset: (x: 8pt, y: 4pt))[ + #text(size: 0.85em, weight: "bold")[#filename]] + #body + ] +} + +#let quarto-code-annotation(annotations, color: luma(60), body) = { show raw.where(block: true): it => it - // Handle annotations for native raw blocks (no-op when annotations is empty) show raw.line: it => { let annote-num = annotations.at(str(it.number), default: none) if annote-num != none { - box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num)] + box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num, color: color)] } else { it } } - block(fill: fill, width: 100%, inset: 0pt, radius: 2pt, clip: true)[ - #if filename != none { - block( - fill: fill.darken(10%), - width: 100%, - inset: (x: 8pt, y: 4pt), - )[#text(size: 0.85em, weight: "bold")[#filename]] - } - #block(inset: 8pt, width: 100%, body) - ] + body } #let quarto-annotation-item(n, content) = { @@ -64,8 +77,11 @@ ] } -// Route all native raw code blocks through the unified wrapper -#show raw.where(block: true): it => quarto-code-block(it) +// Style native raw code blocks with default inset, radius, and stroke +#show raw.where(block: true): it => block( + fill: luma(230), width: 100%, inset: 8pt, radius: 2pt, + stroke: 0.5pt + luma(200), it, +) #let block_with_new_content(old_block, new_content) = { let fields = old_block.fields() diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd index bc2a59e174..ab87ca92c1 100644 --- a/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd @@ -10,7 +10,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - "quarto-code-block" + - "quarto-code-annotation" - "quarto-annotation-item" - [] --- diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd index bef4754ca4..1941ca9b5d 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd @@ -8,7 +8,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - 'quarto-code-block\(filename:' + - 'quarto-code-filename\(' - [] --- From 9e800ff1c414b825606afcfdabf6f7ab9353282c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:29:59 +0100 Subject: [PATCH 08/15] test: expand Typst code annotation and filename test coverage --- .../code-annotations-cell-jupyter.qmd | 25 ++++++++++++++++++ .../code-annotations-cell-knitr.qmd | 25 ++++++++++++++++++ .../code-annotations-none.qmd | 4 +-- .../code-filename-annotation-cell-jupyter.qmd | 26 +++++++++++++++++++ .../code-filename-annotation-cell-knitr.qmd | 26 +++++++++++++++++++ .../code-filename-annotation-native.qmd | 26 +++++++++++++++++++ .../code-filename-annotation.qmd | 25 ++++++++++++++++++ .../code-filename-cell-jupyter.qmd | 18 +++++++++++++ .../code-filename-cell-knitr.qmd | 18 +++++++++++++ .../code-filename/code-filename-native.qmd | 19 ++++++++++++++ .../typst/code-filename/code-filename.qmd | 2 +- 11 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd create mode 100644 tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-cell-jupyter.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-cell-knitr.qmd create mode 100644 tests/docs/smoke-all/typst/code-filename/code-filename-native.qmd diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd new file mode 100644 index 0000000000..9fa5d03993 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd @@ -0,0 +1,25 @@ +--- +title: Code Annotations (Cell, Jupyter) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - "quarto-circled-number" + - "quarto-annotation-item" + - "annotations:" + - [] +--- + +```{python} +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd new file mode 100644 index 0000000000..f7b6fc231f --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd @@ -0,0 +1,25 @@ +--- +title: Code Annotations (Cell, Knitr) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - "quarto-circled-number" + - "quarto-annotation-item" + - "annotations:" + - [] +--- + +```{r} +x <- 1 # <1> +y <- 2 +z <- x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd index eb28056cad..d5b39ac735 100644 --- a/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-none.qmd @@ -10,8 +10,8 @@ _quarto: ensureTypstFileRegexMatches: - [] - - - "quarto-annotation-item" - - "quarto-circled-number" + - '#quarto-annotation-item\(' + - 'annotations: \("' --- ```python diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd new file mode 100644 index 0000000000..36c58f8358 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd @@ -0,0 +1,26 @@ +--- +title: Code Filename + Annotation (Cell, Jupyter) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - "quarto-annotation-item" + - "annotations:" + - [] +--- + +```{python} +#| filename: "hello.py" +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd new file mode 100644 index 0000000000..e8015b3e63 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd @@ -0,0 +1,26 @@ +--- +title: Code Filename + Annotation (Cell, Knitr) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - "quarto-annotation-item" + - "annotations:" + - [] +--- + +```{r} +#| filename: "hello.R" +x <- 1 # <1> +y <- 2 +z <- x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd new file mode 100644 index 0000000000..fccf7d767c --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd @@ -0,0 +1,26 @@ +--- +title: Code Filename + Annotation (Native) +format: + typst: + keep-typ: true + code-annotations: true + syntax-highlighting: idiomatic +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - "quarto-code-annotation" + - "quarto-annotation-item" + - [] +--- + +```{.python filename="example.py"} +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd new file mode 100644 index 0000000000..040f4c5a11 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd @@ -0,0 +1,25 @@ +--- +title: Code Filename + Annotation (Code Block) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - "quarto-annotation-item" + - "annotations:" + - [] +--- + +```{.python filename="example.py"} +x = 1 # <1> +y = 2 +z = x + y # <2> +``` + +1. Assign x. +2. Compute sum. diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-cell-jupyter.qmd new file mode 100644 index 0000000000..fc839eef4f --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-cell-jupyter.qmd @@ -0,0 +1,18 @@ +--- +title: Code Filename (Cell, Jupyter) +format: + typst: + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - [] +--- + +```{python} +#| filename: "hello.py" +print("Hello, world!") +``` diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-cell-knitr.qmd new file mode 100644 index 0000000000..867209f751 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-cell-knitr.qmd @@ -0,0 +1,18 @@ +--- +title: Code Filename (Cell, Knitr) +format: + typst: + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - [] +--- + +```{r} +#| filename: "hello.R" +print("Hello, world!") +``` diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-native.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-native.qmd new file mode 100644 index 0000000000..5f6fa3caf8 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-native.qmd @@ -0,0 +1,19 @@ +--- +title: Code Filename (Native) +format: + typst: + keep-typ: true + syntax-highlighting: idiomatic +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - [] +--- + +```{.python filename="example.py"} +def hello(): + print("Hello, world!") +``` diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd index 1941ca9b5d..db7f158771 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd @@ -1,5 +1,5 @@ --- -title: Code Filename Bar +title: Code Filename (Code Block) format: typst: keep-typ: true From 8241543b8e6bd7be987c7c2cde7a9358131be107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:03:55 +0100 Subject: [PATCH 09/15] fix: harden Typst code annotation and filename escaping --- src/resources/filters/customnodes/decoratedcodeblock.lua | 3 ++- src/resources/filters/quarto-pre/code-annotation.lua | 8 +------- .../formats/typst/pandoc/quarto/definitions.typ | 9 ++++++--- .../typst/syntax-highlighting/skylighting-default.qmd | 2 +- .../syntax-highlighting/skylighting-line-numbers.qmd | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/resources/filters/customnodes/decoratedcodeblock.lua b/src/resources/filters/customnodes/decoratedcodeblock.lua index f355f37834..a0c7c1dd49 100644 --- a/src/resources/filters/customnodes/decoratedcodeblock.lua +++ b/src/resources/filters/customnodes/decoratedcodeblock.lua @@ -197,8 +197,9 @@ _quarto.ast.add_renderer("DecoratedCodeBlock", CodeBlock = render_folded_block }) or pandoc.Blocks({}) local blocks = pandoc.Blocks({}) + local escaped = node.filename:gsub('\\', '\\\\'):gsub('"', '\\"') blocks:insert(pandoc.RawBlock("typst", - '#quarto-code-filename("' .. node.filename .. '")[')) + '#quarto-code-filename("' .. escaped .. '")[')) blocks:extend(rendered) blocks:insert(pandoc.RawBlock("typst", "]")) return pandoc.Div(blocks) diff --git a/src/resources/filters/quarto-pre/code-annotation.lua b/src/resources/filters/quarto-pre/code-annotation.lua index 19a41adecc..20a5aa809a 100644 --- a/src/resources/filters/quarto-pre/code-annotation.lua +++ b/src/resources/filters/quarto-pre/code-annotation.lua @@ -321,10 +321,6 @@ function processAnnotation(line, annoteNumber, annotationProvider) return stripped end -function processTypstAnnotation(line, annoteNumber, annotationProvider) - local stripped = annotationProvider.stripAnnotation(line, annoteNumber) - return stripped -end function code_meta() return { @@ -419,8 +415,6 @@ function code_annotations() annotationProcessor = processLaTeXAnnotation elseif _quarto.format.isAsciiDocOutput() then annotationProcessor = processAsciidocAnnotation - elseif _quarto.format.isTypstOutput() then - annotationProcessor = processTypstAnnotation end -- resolve annotations @@ -564,7 +558,7 @@ function code_annotations() local annotationNumber = block.start + i - 1 local annoteId = toAnnoteId(annotationNumber) if pendingAnnotations[annoteId] then - local content = pandoc.utils.stringify(v[1]) + local content = pandoc.write(pandoc.Pandoc({v[1]}), "typst") annotationBlocks:insert(pandoc.RawBlock("typst", "#quarto-annotation-item(" .. tostring(annotationNumber) .. ", [" .. content .. "])")) end diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index cefb9453a8..cf3287037e 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -35,11 +35,14 @@ // Derive a contrasting annotation colour from a background fill. // Light backgrounds get dark circles; dark backgrounds get light circles. -// Uses relative luminance: 0.2126R + 0.7152G + 0.0722B. #let quarto-annote-color(bg) = { if type(bg) == color { - let (r, g, b, ..) = bg.components(alpha: false) - let lum = 0.2126 * r / 100% + 0.7152 * g / 100% + 0.0722 * b / 100% + let comps = bg.components(alpha: false) + let lum = if comps.len() == 1 { + comps.at(0) / 100% + } else { + 0.2126 * comps.at(0) / 100% + 0.7152 * comps.at(1) / 100% + 0.0722 * comps.at(2) / 100% + } if lum < 0.5 { luma(200) } else { luma(60) } } else { luma(60) diff --git a/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-default.qmd b/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-default.qmd index de81bcf646..9dd63241bb 100644 --- a/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-default.qmd +++ b/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-default.qmd @@ -14,7 +14,7 @@ _quarto: - "#let EndLine" # Quarto override with proper block styling and arrow theme bgcolor - 'let bgcolor = rgb\("#f1f3f5"\)' - - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)' + - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0\.5pt \+ luma\(200\), blocks\)' - ["```python"] --- diff --git a/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-line-numbers.qmd b/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-line-numbers.qmd index af2fe7e9f3..b7e43d82ea 100644 --- a/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-line-numbers.qmd +++ b/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-line-numbers.qmd @@ -14,7 +14,7 @@ _quarto: - '#Skylighting\(number: true' - "#KeywordTok" # Quarto override with proper block styling - - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)' + - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0\.5pt \+ luma\(200\), blocks\)' - ["```python"] --- From 9416ad16f6109ce8eaff3554cf25a0ae39d38d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:58:39 +0100 Subject: [PATCH 10/15] test: tweak test files --- .../code-filename-annotation-cell-jupyter.qmd | 9 +++------ .../code-filename-annotation-cell-knitr.qmd | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd index 36c58f8358..c8ae00ad73 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd @@ -9,7 +9,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - 'quarto-code-filename\(' + - 'quarto-code-filename\("hello\.py"\)\[\s*#Skylighting\(annotations:' - "quarto-annotation-item" - "annotations:" - [] @@ -17,10 +17,7 @@ _quarto: ```{python} #| filename: "hello.py" -x = 1 # <1> -y = 2 -z = x + y # <2> +print("Hello, world!") # <1> ``` -1. Assign x. -2. Compute sum. +1. Print a greeting. diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd index e8015b3e63..9e5fd673ec 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd @@ -9,7 +9,7 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - 'quarto-code-filename\(' + - 'quarto-code-filename\("hello\.R"\)\[\s*#Skylighting\(annotations:' - "quarto-annotation-item" - "annotations:" - [] @@ -17,10 +17,7 @@ _quarto: ```{r} #| filename: "hello.R" -x <- 1 # <1> -y <- 2 -z <- x + y # <2> +print("Hello, world!") # <1> ``` -1. Assign x. -2. Compute sum. +1. Print a greeting. From 78a54f80607282aa2930b7ffe1ceb956d7ca9360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:59:34 +0100 Subject: [PATCH 11/15] fix: merge parent block from code cell with annotation marker regex for Skylighting Update the annotation marker regex to support optional #block[ and #quarto-code-filename(...) wrappers in Skylighting call sites. This improves compatibility with various annotation formats. --- src/format/typst/format-typst.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index 40abe93e04..c4cc575082 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -207,7 +207,7 @@ function skylightingPostProcessor(brandBgColor?: string) { // Annotation markers emitted by the Lua filter as Typst comments const annotationMarkerRe = - /\/\/ quarto-code-annotations: (\([^)]*\))\n\s*#Skylighting\(/g; + /\/\/ quarto-code-annotations: (\([^)]*\))\n(\s*(?:#block\[\s*)*(?:#quarto-code-filename\([^\n]*\)\[\s*)?)#Skylighting\(/g; return async (output: string) => { let content = Deno.readTextFileSync(output); @@ -260,10 +260,11 @@ function skylightingPostProcessor(brandBgColor?: string) { } } - // Merge annotation markers into Skylighting call sites + // Merge annotation markers into Skylighting call sites, including + // optional #block[ wrappers and #quarto-code-filename(...)[ wrappers. const merged = content.replace( annotationMarkerRe, - "#Skylighting(annotations: $1, ", + "$2#Skylighting(annotations: $1, ", ); if (merged !== content) { content = merged; From c0f14486008b5beb10af21496f5e062caf0a7a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:37:08 +0100 Subject: [PATCH 12/15] fix: improve semantic structure by linking code and annotation --- src/format/typst/format-typst.ts | 15 ++++++---- .../filters/quarto-pre/code-annotation.lua | 28 ++++++++++-------- .../typst/pandoc/quarto/definitions.typ | 29 ++++++++++++++----- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index c4cc575082..0466cf1255 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -207,7 +207,7 @@ function skylightingPostProcessor(brandBgColor?: string) { // Annotation markers emitted by the Lua filter as Typst comments const annotationMarkerRe = - /\/\/ quarto-code-annotations: (\([^)]*\))\n(\s*(?:#block\[\s*)*(?:#quarto-code-filename\([^\n]*\)\[\s*)?)#Skylighting\(/g; + /\/\/ quarto-code-annotations: ([\w-]*) (\([^)]*\))\n(\s*(?:#block\[\s*)*(?:#quarto-code-filename\([^\n]*\)\[\s*)?)#Skylighting\(/g; return async (output: string) => { let content = Deno.readTextFileSync(output); @@ -231,10 +231,10 @@ function skylightingPostProcessor(brandBgColor?: string) { ); } - // Add annotations parameter to function signature + // Add cell-id and annotations parameters to function signature fn = fn.replace( "start: 1, sourcelines)", - "start: 1, annotations: (:), sourcelines)", + "start: 1, cell-id: \"\", annotations: (:), sourcelines)", ); // Move lnum increment outside if-number block (always track position) @@ -248,7 +248,12 @@ function skylightingPostProcessor(brandBgColor?: string) { "blocks = blocks + ln + EndLine()", `let annote-num = annotations.at(str(lnum), default: none) if annote-num != none { - blocks = blocks + box(width: 100%)[#ln #h(1fr) #quarto-circled-number(annote-num, color: quarto-annote-color(bgcolor))] + EndLine() + if cell-id != "" { + let lbl = cell-id + "-annote-" + str(annote-num) + 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) #quarto-circled-number(annote-num, color: quarto-annote-color(bgcolor))] + EndLine() + } } else { blocks = blocks + ln + EndLine() }`, @@ -264,7 +269,7 @@ function skylightingPostProcessor(brandBgColor?: string) { // optional #block[ wrappers and #quarto-code-filename(...)[ wrappers. const merged = content.replace( annotationMarkerRe, - "$2#Skylighting(annotations: $1, ", + "$3#Skylighting(cell-id: \"$1\", annotations: $2, ", ); if (merged !== content) { content = merged; diff --git a/src/resources/filters/quarto-pre/code-annotation.lua b/src/resources/filters/quarto-pre/code-annotation.lua index 20a5aa809a..b2ddf109d2 100644 --- a/src/resources/filters/quarto-pre/code-annotation.lua +++ b/src/resources/filters/quarto-pre/code-annotation.lua @@ -108,14 +108,14 @@ end -- Skylighting mode: emit a Typst comment that the TS post-processor -- will merge into the Skylighting call site. -local function typstAnnotationMarker(annotations) +local function typstAnnotationMarker(annotations, cellId) local dict = typstAnnotationsDict(annotations) - return pandoc.RawBlock("typst", "// quarto-code-annotations: " .. dict) + return pandoc.RawBlock("typst", "// quarto-code-annotations: " .. (cellId or "") .. " " .. dict) end -- Native/none mode: wrap a CodeBlock in #quarto-code-annotation(annotations)[...]. -- raw.line numbers always start at 1 regardless of startFrom, so adjust keys. -local function wrapTypstAnnotatedCode(codeBlock, annotations) +local function wrapTypstAnnotatedCode(codeBlock, annotations, cellId) local startFrom = tonumber(codeBlock.attr.attributes['startFrom']) or 1 local adjustedAnnotations = {} for annoteId, lineNumbers in pairs(annotations) do @@ -126,6 +126,10 @@ local function wrapTypstAnnotatedCode(codeBlock, annotations) adjustedAnnotations[annoteId] = adjusted end local dict = typstAnnotationsDict(adjustedAnnotations) + local cellIdParam = "" + if cellId and cellId ~= "" then + cellIdParam = ", cell-id: \"" .. cellId .. "\"" + end local lang = codeBlock.attr.classes[1] or "" local code = codeBlock.text local maxBackticks = 2 @@ -133,7 +137,7 @@ local function wrapTypstAnnotatedCode(codeBlock, annotations) maxBackticks = math.max(maxBackticks, #seq) end local fence = string.rep("`", maxBackticks + 1) - local raw = "#quarto-code-annotation(" .. dict .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" + local raw = "#quarto-code-annotation(" .. dict .. cellIdParam .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" return pandoc.RawBlock("typst", raw) end @@ -497,9 +501,9 @@ function code_annotations() and pendingAnnotations and next(pendingAnnotations) ~= nil then if param(constants.kSyntaxHighlighting, true) then block.content[1].content[1] = codeCell - block.content[1].content:insert(1, typstAnnotationMarker(pendingAnnotations)) + block.content[1].content:insert(1, typstAnnotationMarker(pendingAnnotations, pendingCellId)) else - block.content[1].content[1] = wrapTypstAnnotatedCode(codeCell, pendingAnnotations) + block.content[1].content[1] = wrapTypstAnnotatedCode(codeCell, pendingAnnotations, pendingCellId) end else block.content[1].content[1] = codeCell @@ -535,10 +539,10 @@ function code_annotations() and codeAnnotations ~= constants.kCodeAnnotationStyleNone and pendingAnnotations and next(pendingAnnotations) ~= nil then if param(constants.kSyntaxHighlighting, true) then - outputBlock(typstAnnotationMarker(pendingAnnotations)) + outputBlock(typstAnnotationMarker(pendingAnnotations, pendingCellId)) outputBlock(codeCell) else - outputBlock(wrapTypstAnnotatedCode(codeCell, pendingAnnotations)) + outputBlock(wrapTypstAnnotatedCode(codeCell, pendingAnnotations, pendingCellId)) end else outputBlock(codeCell) @@ -560,7 +564,7 @@ function code_annotations() if pendingAnnotations[annoteId] then local content = pandoc.write(pandoc.Pandoc({v[1]}), "typst") annotationBlocks:insert(pandoc.RawBlock("typst", - "#quarto-annotation-item(" .. tostring(annotationNumber) .. ", [" .. content .. "])")) + "#quarto-annotation-item(\"" .. (pendingCellId or "") .. "\", " .. tostring(annotationNumber) .. ", [" .. content .. "])")) end end @@ -574,7 +578,7 @@ function code_annotations() if useSkylighting then return nil else - return wrapTypstAnnotatedCode(el, pendingAnnotations) + return wrapTypstAnnotatedCode(el, pendingAnnotations, pendingCellId) end end end @@ -584,12 +588,12 @@ function code_annotations() if is_custom_node(resolvedCell) then local custom = _quarto.ast.resolve_custom_data(resolvedCell) or pandoc.Div({}) if useSkylighting then - custom.content:insert(1, typstAnnotationMarker(pendingAnnotations)) + custom.content:insert(1, typstAnnotationMarker(pendingAnnotations, pendingCellId)) end custom.content:insert(dlDiv) else if useSkylighting then - resolvedCell.content:insert(1, typstAnnotationMarker(pendingAnnotations)) + resolvedCell.content:insert(1, typstAnnotationMarker(pendingAnnotations, pendingCellId)) end resolvedCell.content:insert(dlDiv) end diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index cf3287037e..ee210aa65f 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -59,12 +59,17 @@ ] } -#let quarto-code-annotation(annotations, color: luma(60), body) = { +#let quarto-code-annotation(annotations, cell-id: "", color: luma(60), body) = { show raw.where(block: true): it => it show raw.line: it => { let annote-num = annotations.at(str(it.number), default: none) if annote-num != none { - box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num, color: color)] + if cell-id != "" { + let lbl = cell-id + "-annote-" + str(annote-num) + box(width: 100%)[#it #h(1fr) #link(label(lbl))[#quarto-circled-number(annote-num, color: color)] #label(lbl + "-back")] + } else { + box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num, color: color)] + } } else { it } @@ -72,12 +77,20 @@ body } -#let quarto-annotation-item(n, content) = { - block(above: 0.4em, below: 0.4em)[ - #quarto-circled-number(n) - #h(0.4em) - #content - ] +#let quarto-annotation-item(cell-id, n, content) = { + if cell-id != "" { + [#block(above: 0.4em, below: 0.4em)[ + #link(label(cell-id + "-annote-" + str(n) + "-back"))[#quarto-circled-number(n)] + #h(0.4em) + #content + ] #label(cell-id + "-annote-" + str(n))] + } else { + block(above: 0.4em, below: 0.4em)[ + #quarto-circled-number(n) + #h(0.4em) + #content + ] + } } // Style native raw code blocks with default inset, radius, and stroke From ac932e9a49df8bc8027f08e0f1a71d2eaa3e2bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:39:23 +0100 Subject: [PATCH 13/15] test: update tests with semantic links --- .../typst/code-annotations/code-annotations-cell-jupyter.qmd | 1 + .../typst/code-annotations/code-annotations-cell-knitr.qmd | 1 + .../typst/code-annotations/code-annotations-native.qmd | 1 + .../docs/smoke-all/typst/code-annotations/code-annotations.qmd | 1 + .../code-filename/code-filename-annotation-cell-jupyter.qmd | 3 ++- .../code-filename/code-filename-annotation-cell-knitr.qmd | 3 ++- .../typst/code-filename/code-filename-annotation-native.qmd | 1 + .../smoke-all/typst/code-filename/code-filename-annotation.qmd | 1 + 8 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd index 9fa5d03993..609c41dfc9 100644 --- a/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd @@ -11,6 +11,7 @@ _quarto: - - "quarto-circled-number" - "quarto-annotation-item" + - "cell-id:" - "annotations:" - [] --- diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd index f7b6fc231f..f2883a45b6 100644 --- a/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd @@ -11,6 +11,7 @@ _quarto: - - "quarto-circled-number" - "quarto-annotation-item" + - "cell-id:" - "annotations:" - [] --- diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd index ab87ca92c1..51e0205ae0 100644 --- a/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd @@ -11,6 +11,7 @@ _quarto: ensureTypstFileRegexMatches: - - "quarto-code-annotation" + - "cell-id:" - "quarto-annotation-item" - [] --- diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd index d8ff469d81..a9c6f31227 100644 --- a/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd @@ -11,6 +11,7 @@ _quarto: - - "quarto-circled-number" - "quarto-annotation-item" + - "cell-id:" - "annotations:" - [] --- diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd index c8ae00ad73..25add89663 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd @@ -9,8 +9,9 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - 'quarto-code-filename\("hello\.py"\)\[\s*#Skylighting\(annotations:' + - 'quarto-code-filename\("hello\.py"\)\[\s*#Skylighting\(cell-id:' - "quarto-annotation-item" + - "cell-id:" - "annotations:" - [] --- diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd index 9e5fd673ec..76372384a9 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd @@ -9,8 +9,9 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - 'quarto-code-filename\("hello\.R"\)\[\s*#Skylighting\(annotations:' + - 'quarto-code-filename\("hello\.R"\)\[\s*#Skylighting\(cell-id:' - "quarto-annotation-item" + - "cell-id:" - "annotations:" - [] --- diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd index fccf7d767c..2789e01631 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd @@ -12,6 +12,7 @@ _quarto: - - 'quarto-code-filename\(' - "quarto-code-annotation" + - "cell-id:" - "quarto-annotation-item" - [] --- diff --git a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd index 040f4c5a11..2182af5069 100644 --- a/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd @@ -11,6 +11,7 @@ _quarto: - - 'quarto-code-filename\(' - "quarto-annotation-item" + - "cell-id:" - "annotations:" - [] --- From 5c8b344c1c57af83160409953a0289e3e7a53d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:15:03 +0100 Subject: [PATCH 14/15] fix: ensure back-labels are emitted only once --- src/format/typst/format-typst.ts | 17 +++++++++++++++-- .../filters/quarto-pre/code-annotation.lua | 16 ++++++---------- .../formats/typst/pandoc/quarto/definitions.typ | 17 ++++++++++++++++- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index 0466cf1255..e4a1abd579 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -210,7 +210,7 @@ function skylightingPostProcessor(brandBgColor?: string) { /\/\/ quarto-code-annotations: ([\w-]*) (\([^)]*\))\n(\s*(?:#block\[\s*)*(?:#quarto-code-filename\([^\n]*\)\[\s*)?)#Skylighting\(/g; return async (output: string) => { - let content = Deno.readTextFileSync(output); + let content = Deno.readTextFileSync(output).replace(/\r\n/g, "\n"); let changed = false; const match = skylightingFnRe.exec(content); @@ -243,6 +243,14 @@ function skylightingPostProcessor(brandBgColor?: string) { "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", + ); + // Add annotation rendering per line (derive circle colour from bgcolor) fn = fn.replace( "blocks = blocks + ln + EndLine()", @@ -250,7 +258,12 @@ function skylightingPostProcessor(brandBgColor?: string) { if annote-num != none { if cell-id != "" { let lbl = cell-id + "-annote-" + str(annote-num) - 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() + 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() } diff --git a/src/resources/filters/quarto-pre/code-annotation.lua b/src/resources/filters/quarto-pre/code-annotation.lua index b2ddf109d2..ddae0f6ad7 100644 --- a/src/resources/filters/quarto-pre/code-annotation.lua +++ b/src/resources/filters/quarto-pre/code-annotation.lua @@ -126,10 +126,6 @@ local function wrapTypstAnnotatedCode(codeBlock, annotations, cellId) adjustedAnnotations[annoteId] = adjusted end local dict = typstAnnotationsDict(adjustedAnnotations) - local cellIdParam = "" - if cellId and cellId ~= "" then - cellIdParam = ", cell-id: \"" .. cellId .. "\"" - end local lang = codeBlock.attr.classes[1] or "" local code = codeBlock.text local maxBackticks = 2 @@ -137,7 +133,9 @@ local function wrapTypstAnnotatedCode(codeBlock, annotations, cellId) maxBackticks = math.max(maxBackticks, #seq) end local fence = string.rep("`", maxBackticks + 1) - local raw = "#quarto-code-annotation(" .. dict .. cellIdParam .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" + local raw = "#quarto-code-annotation(" .. dict + .. (cellId and cellId ~= "" and (", cell-id: \"" .. cellId .. "\"") or "") + .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" return pandoc.RawBlock("typst", raw) end @@ -568,14 +566,12 @@ function code_annotations() end end - local useSkylighting = param(constants.kSyntaxHighlighting, true) - if pendingCodeCell ~= nil then local resolvedCell = _quarto.ast.walk(pendingCodeCell, { CodeBlock = function(el) if el.attr.classes:find('cell-code') or el.attr.classes:find(constants.kDataCodeAnnonationClz) then - if useSkylighting then + if param(constants.kSyntaxHighlighting, true) then return nil else return wrapTypstAnnotatedCode(el, pendingAnnotations, pendingCellId) @@ -587,12 +583,12 @@ function code_annotations() local dlDiv = pandoc.Div(annotationBlocks, pandoc.Attr("", {constants.kCellAnnotationClass})) if is_custom_node(resolvedCell) then local custom = _quarto.ast.resolve_custom_data(resolvedCell) or pandoc.Div({}) - if useSkylighting then + if param(constants.kSyntaxHighlighting, true) then custom.content:insert(1, typstAnnotationMarker(pendingAnnotations, pendingCellId)) end custom.content:insert(dlDiv) else - if useSkylighting then + if param(constants.kSyntaxHighlighting, true) then resolvedCell.content:insert(1, typstAnnotationMarker(pendingAnnotations, pendingCellId)) end resolvedCell.content:insert(dlDiv) diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index ee210aa65f..04c6851610 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -60,13 +60,28 @@ } #let quarto-code-annotation(annotations, cell-id: "", color: luma(60), body) = { + // Build a set of first-line positions per annotation number so that + // back-labels are only emitted once (avoiding duplicate labels when + // one annotation spans multiple lines). + let first-lines = (:) + for (line, num) in annotations { + let key = str(num) + if key not in first-lines or int(line) < int(first-lines.at(key)) { + first-lines.insert(key, line) + } + } show raw.where(block: true): it => it show raw.line: it => { let annote-num = annotations.at(str(it.number), default: none) if annote-num != none { if cell-id != "" { let lbl = cell-id + "-annote-" + str(annote-num) - box(width: 100%)[#it #h(1fr) #link(label(lbl))[#quarto-circled-number(annote-num, color: color)] #label(lbl + "-back")] + let is-first = first-lines.at(str(annote-num), default: none) == str(it.number) + if is-first { + box(width: 100%)[#it #h(1fr) #link(label(lbl))[#quarto-circled-number(annote-num, color: color)] #label(lbl + "-back")] + } else { + box(width: 100%)[#it #h(1fr) #link(label(lbl))[#quarto-circled-number(annote-num, color: color)]] + } } else { box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num, color: color)] } From 994a87803fee7c4887464b78d515794236d15339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:15:56 +0100 Subject: [PATCH 15/15] test: update block styling to include stroke in monospace tests --- .../brand-monospace-block-no-bg/brand-monospace-block-no-bg.qmd | 2 +- .../brand-monospace-inheritance/brand-monospace-inheritance.qmd | 2 +- .../brand-monospace-with-theme/brand-monospace-with-theme.qmd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-block-no-bg/brand-monospace-block-no-bg.qmd b/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-block-no-bg/brand-monospace-block-no-bg.qmd index 3e6bf00c79..6c84975a01 100644 --- a/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-block-no-bg/brand-monospace-block-no-bg.qmd +++ b/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-block-no-bg/brand-monospace-block-no-bg.qmd @@ -17,7 +17,7 @@ _quarto: # 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\)' + - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0\.5pt \+ luma\(200\), blocks\)' # No brand background-color show rule (not configured) - ['^#show raw\.where\(block: true\): set block\(fill:'] --- diff --git a/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-inheritance/brand-monospace-inheritance.qmd b/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-inheritance/brand-monospace-inheritance.qmd index acdf6d9ade..4b01d4ceb3 100644 --- a/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-inheritance/brand-monospace-inheritance.qmd +++ b/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-inheritance/brand-monospace-inheritance.qmd @@ -18,7 +18,7 @@ _quarto: - '^#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\)' + - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0\.5pt \+ luma\(200\), 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\)$' diff --git a/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-with-theme/brand-monospace-with-theme.qmd b/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-with-theme/brand-monospace-with-theme.qmd index 0248936596..d4c56fcff5 100644 --- a/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-with-theme/brand-monospace-with-theme.qmd +++ b/tests/docs/smoke-all/typst/syntax-highlighting/brand-monospace-with-theme/brand-monospace-with-theme.qmd @@ -18,7 +18,7 @@ _quarto: - '^#show raw\.where\(block: true\): set par\(leading: 0\.65em\)$' # Quarto Skylighting override uses brand bg (not espresso theme bg) - 'let bgcolor = rgb\("#282a36"\)' - - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)' + - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, stroke: 0\.5pt \+ luma\(200\), blocks\)' # Brand monospace-inline properties - '^#show raw\.where\(block: false\): set text\(weight: 500, fill: rgb\("#6c3483"\), \)$' - '^#show raw\.where\(block: false\): content => highlight\(fill: rgb\("#e8e0f0"\), content\)$'