diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 4c11a6001e4..73fe7cd2f28 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -1,3 +1,7 @@ All changes included in 1.11: +## Formats +### `typst` + +- ([#14170](https://github.com/quarto-dev/quarto-cli/pull/14170)): Add support for code annotations and filename features in Typst output. (author: @mcanouil) diff --git a/src/command/render/filters.ts b/src/command/render/filters.ts index 863d3d9603a..7ad4ca4915e 100644 --- a/src/command/render/filters.ts +++ b/src/command/render/filters.ts @@ -25,6 +25,7 @@ import { kFigResponsive, kFormatIdentifier, kHeaderIncludes, + kHighlightStyle, kHtmlMathMethod, kHtmlPreTagProcessing, kHtmlTableProcessing, @@ -50,11 +51,13 @@ import { kRemoveHidden, kResourcePath, kShortcodes, + kSyntaxHighlighting, kTblColwidths, 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; + // Object themes (light/dark) resolve to a single .theme file for Typst + // (resolveTextHighlightStyle), so Skylighting is active for them too. + const skylighting = 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, }; }; diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index 71f06570226..07500749050 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -187,40 +187,129 @@ 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. + // Between the marker and the Skylighting call there may be #block[ (cell + // divs), #figure([ (crossref floats), and #quarto-code-filename( wrappers. + const annotationMarkerRe = + /\/\/ quarto-code-annotations: (\S*) (\([^)]*\))\n(\s*(?:(?:#block\[|#figure\(\[)\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, 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 and track a 1-based + // annotation position independent of `start` (annotation dict keys + // are always 1-based, whether or not Pandoc passes `start:`) + fn = fn.replace( + /if number \{\n\s+lnum = lnum \+ 1\n/, + "lnum = lnum + 1\n annote-pos = annote-pos + 1\n if number {\n", + ); - // Override bgcolor with brand monospace-block background-color - if (brandBgColor) { + // Initialise the annotation position counter and 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 bgcolor = rgb\("[^"]*"\)/, - `let bgcolor = rgb("${brandBgColor}")`, + /let lnum = start - 1\n/, + "let lnum = start - 1\n let annote-pos = 0\n let seen-annotes = (:)\n", ); + + // Add annotation rendering per line (derive circle colour from bgcolor). + // quarto-annote-link degrades to an unlinked circle when the annotation + // item label is missing (e.g. no trailing ordered list). + fn = fn.replace( + "blocks = blocks + ln + EndLine()", + `let annote-num = annotations.at(str(annote-pos), 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) #quarto-annote-link(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-annote-link(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; + } + + // Strip any markers that could not be merged (e.g. the code block was + // wrapped in output the merge regex does not recognise) so they don't + // leak into keep-typ output. Rendering degrades gracefully via + // quarto-annote-link. + const stripped = content.replace( + /^[^\S\n]*\/\/ quarto-code-annotations:[^\n]*\n/gm, + "", + ); + if (stripped !== content) { + content = stripped; + changed = true; } - if (fn !== match[1]) { - Deno.writeTextFileSync(output, content.replace(match[1], fn)); + if (changed) { + Deno.writeTextFileSync(output, content); } }; } diff --git a/src/resources/filters/customnodes/decoratedcodeblock.lua b/src/resources/filters/customnodes/decoratedcodeblock.lua index 5aeefb0a55b..936cb437cfb 100644 --- a/src/resources/filters/customnodes/decoratedcodeblock.lua +++ b/src/resources/filters/customnodes/decoratedcodeblock.lua @@ -180,3 +180,33 @@ _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('[\\"\n\r\t]', { + ['\\'] = '\\\\', + ['"'] = '\\"', + ['\n'] = '\\n', + ['\r'] = '\\r', + ['\t'] = '\\t', + }) + blocks:insert(pandoc.RawBlock("typst", + '#quarto-code-filename("' .. escaped .. '")[')) + blocks:extend(rendered) + blocks:insert(pandoc.RawBlock("typst", "]")) + return pandoc.Div(blocks) + end) diff --git a/src/resources/filters/modules/constants.lua b/src/resources/filters/modules/constants.lua index f142341493d..1bda001d8a9 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' @@ -201,6 +202,7 @@ return { kAsciidocNativeCites = kAsciidocNativeCites, kShowNotes = kShowNotes, kProjectResolverIgnore = kProjectResolverIgnore, + kSyntaxHighlighting = kSyntaxHighlighting, kCodeAnnotationsParam = kCodeAnnotationsParam, kDataCodeCellTarget = kDataCodeCellTarget, kDataCodeCellLines = kDataCodeCellLines, diff --git a/src/resources/filters/modules/typst-code-annotations.lua b/src/resources/filters/modules/typst-code-annotations.lua new file mode 100644 index 00000000000..b64f5630ce4 --- /dev/null +++ b/src/resources/filters/modules/typst-code-annotations.lua @@ -0,0 +1,77 @@ +-- typst-code-annotations.lua +-- Copyright (C) 2026 Posit Software, PBC + +-- Typst annotation helpers for code blocks. +-- Loaded via require() so locals stay out of the bundled main.lua scope. + +local function _main() + + -- 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 + + -- Annotation line numbers are collected display-based (shifted by the + -- startFrom attribute or the fenced-echo start line). Typst-side rendering + -- always counts source lines from 1, so shift keys back with the same + -- offset used at collection time. + local function adjustAnnotations(annotations, offset) + offset = offset or 1 + local adjusted = {} + for annoteId, lineNumbers in pairs(annotations) do + local list = pandoc.List({}) + for _, lineNo in ipairs(lineNumbers) do + list:insert(lineNo - offset + 1) + end + adjusted[annoteId] = list + end + return adjusted + end + + -- Skylighting mode: emit a Typst comment that the TS post-processor + -- will merge into the Skylighting call site. + local function typstAnnotationMarker(annotations, cellId, offset) + local dict = typstAnnotationsDict(adjustAnnotations(annotations, offset)) + 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, cellId, offset) + local dict = typstAnnotationsDict(adjustAnnotations(annotations, offset)) + 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-annotation(" .. dict + .. (cellId and cellId ~= "" and (", cell-id: \"" .. cellId .. "\"") or "") + .. ")[" .. fence .. lang .. "\n" .. code .. "\n" .. fence .. "]" + return pandoc.RawBlock("typst", raw) + end + + return { + typstAnnotationsDict = typstAnnotationsDict, + typstAnnotationMarker = typstAnnotationMarker, + wrapTypstAnnotatedCode = wrapTypstAnnotatedCode, + } +end + +return _main() diff --git a/src/resources/filters/quarto-pre/code-annotation.lua b/src/resources/filters/quarto-pre/code-annotation.lua index 8a56df4cae3..8437440d048 100644 --- a/src/resources/filters/quarto-pre/code-annotation.lua +++ b/src/resources/filters/quarto-pre/code-annotation.lua @@ -78,7 +78,7 @@ local function toAnnoteId(number) end local function latexListPlaceholder(number) - return '5CB6E08D-list-annote-' .. number + return '5CB6E08D-list-annote-' .. number end local function toLines(s) @@ -154,7 +154,7 @@ local function resolveCellAnnotes(codeBlockEl, processAnnotation) codeBlockEl.text = outputText hasAnnotations = true end - return codeBlockEl, annotations + return codeBlockEl, annotations, offset elseif lang then return codeBlockEl, {} end @@ -260,11 +260,12 @@ 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 code_meta() return { Meta = function(meta) @@ -284,6 +285,8 @@ end -- The actual filter that will look for a code cell and then -- find its annotations, then process the subsequent OL function code_annotations() + local typstAnnotations = require("modules/typst-code-annotations") + -- the localized strings local language = param("language", nil) @@ -313,11 +316,21 @@ function code_annotations() local pendingAnnotations = nil local pendingCellId = nil local pendingCodeCell = nil - - local clearPending = function() + -- Collected annotation keys are display-based. Two different shifts + -- apply when converting them back to physical source lines for Typst: + -- pendingOffset (startFrom or the fenced-echo start line) for code + -- re-emitted from the pre-time text (native wrap path, no fence line), + -- and pendingStartFrom alone for the Skylighting path, whose output + -- still contains the injected fenced-echo line. + local pendingOffset = nil + local pendingStartFrom = nil + + local clearPending = function() pendingAnnotations = nil pendingCellId = nil pendingCodeCell = nil + pendingOffset = nil + pendingStartFrom = nil end local outputBlock = function(block) @@ -361,11 +374,13 @@ function code_annotations() end -- resolve annotations - local resolvedCodeBlock, annotations = resolveCellAnnotes(el, annotationProcessor) + local resolvedCodeBlock, annotations, annotationOffset = resolveCellAnnotes(el, annotationProcessor) if annotations and next(annotations) ~= nil then -- store the annotations and cell info pendingAnnotations = annotations pendingCellId = identifier + pendingOffset = annotationOffset or 1 + pendingStartFrom = tonumber(el.attr.attributes['startFrom']) or 1 -- decorate the cell and return it if codeAnnotations ~= _quarto.modules.constants.kCodeAnnotationStyleNone then @@ -414,6 +429,33 @@ function code_annotations() outputBlock(resolvedBlock) end + elseif _quarto.format.isTypstOutput() and is_custom_node(block, "FloatRefTarget") then + -- Typst: a crossref float (e.g. lst-cap listing) wraps its code in a + -- nested Blocks list, so the standalone CodeBlock branch below never + -- sees it at this level and the following OL would stay unprocessed. + -- Process the float's code here and hold onto the float so the OL + -- handler can attach the marker and emit annotation items after it. + flushPending() + local processedAnnotation = false + local resolvedBlock = _quarto.ast.walk(block, { + CodeBlock = function(el) + local cellId = resolveCellId(el.attr.identifier) + local codeCell = processCodeCell(el, cellId) + if codeCell then + processedAnnotation = true + if codeAnnotations ~= _quarto.modules.constants.kCodeAnnotationStyleNone then + codeCell.attr.identifier = cellId; + end + end + return codeCell + end + }) + if processedAnnotation then + pendingCodeCell = resolvedBlock + else + outputBlock(resolvedBlock or block) + end + elseif block.t == "Div" then local isDecoratedCodeBlock = is_custom_node(block, "DecoratedCodeBlock") if isDecoratedCodeBlock then @@ -421,17 +463,32 @@ 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] - + local cellId = resolveCellId(codeblock.attr.identifier) local codeCell = processCodeCell(codeblock, cellId) if codeCell then if codeAnnotations ~= _quarto.modules.constants.kCodeAnnotationStyleNone then codeCell.attr.identifier = cellId; end - block.content[1].content[1] = codeCell + -- 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 ~= _quarto.modules.constants.kCodeAnnotationStyleNone + and pendingAnnotations and next(pendingAnnotations) ~= nil then + if param(_quarto.modules.constants.kSyntaxHighlighting, true) then + block.content[1].content[1] = codeCell + block.content[1].content:insert(1, typstAnnotations.typstAnnotationMarker(pendingAnnotations, pendingCellId, pendingStartFrom)) + else + block.content[1].content[1] = typstAnnotations.wrapTypstAnnotatedCode(codeCell, pendingAnnotations, pendingCellId, pendingOffset) + end + else + block.content[1].content[1] = codeCell + end outputBlock(block) else outputBlockClearPending(block) @@ -457,7 +514,20 @@ function code_annotations() if codeAnnotations ~= _quarto.modules.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 ~= _quarto.modules.constants.kCodeAnnotationStyleNone + and pendingAnnotations and next(pendingAnnotations) ~= nil then + if param(_quarto.modules.constants.kSyntaxHighlighting, true) then + outputBlock(typstAnnotations.typstAnnotationMarker(pendingAnnotations, pendingCellId, pendingStartFrom)) + outputBlock(codeCell) + else + outputBlock(typstAnnotations.wrapTypstAnnotatedCode(codeCell, pendingAnnotations, pendingCellId, pendingOffset)) + end + else + outputBlock(codeCell) + end else outputBlockClearPending(block) end @@ -465,6 +535,77 @@ 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 ~= _quarto.modules.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.write(pandoc.Pandoc({v[1]}), "typst") + annotationBlocks:insert(pandoc.RawBlock("typst", + "#quarto-annotation-item(\"" .. (pendingCellId or "") .. "\", " .. tostring(annotationNumber) .. ", [" .. content .. "])")) + end + end + + 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(_quarto.modules.constants.kDataCodeAnnonationClz) then + if param(_quarto.modules.constants.kSyntaxHighlighting, true) then + return nil + else + return typstAnnotations.wrapTypstAnnotatedCode(el, pendingAnnotations, pendingCellId, pendingOffset) + end + end + end + }) + + local dlDiv = pandoc.Div(annotationBlocks, pandoc.Attr("", {_quarto.modules.constants.kCellAnnotationClass})) + if is_custom_node(resolvedCell, "FloatRefTarget") then + -- Crossref float: put the marker inside the float (adjacent to + -- the code) but emit the annotation items after the float so + -- they don't land inside the captioned figure. The float's + -- content slot may be a bare block rather than a Blocks list. + local custom = _quarto.ast.resolve_custom_data(resolvedCell) or pandoc.Div({}) + if param(_quarto.modules.constants.kSyntaxHighlighting, true) then + local marker = typstAnnotations.typstAnnotationMarker(pendingAnnotations, pendingCellId, pendingStartFrom) + local floatContent = custom.content + if floatContent ~= nil and floatContent.insert ~= nil then + floatContent:insert(1, marker) + else + custom.content = pandoc.Blocks({ marker, floatContent }) + end + end + outputBlock(resolvedCell) + outputBlock(dlDiv) + elseif is_custom_node(resolvedCell) then + local custom = _quarto.ast.resolve_custom_data(resolvedCell) or pandoc.Div({}) + if param(_quarto.modules.constants.kSyntaxHighlighting, true) then + custom.content:insert(1, typstAnnotations.typstAnnotationMarker(pendingAnnotations, pendingCellId, pendingStartFrom)) + end + custom.content:insert(dlDiv) + outputBlock(resolvedCell) + else + if param(_quarto.modules.constants.kSyntaxHighlighting, true) then + resolvedCell.content:insert(1, typstAnnotations.typstAnnotationMarker(pendingAnnotations, pendingCellId, pendingStartFrom)) + end + resolvedCell.content:insert(dlDiv) + outputBlock(resolvedCell) + end + 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() @@ -493,7 +634,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) @@ -508,7 +649,7 @@ function code_annotations() {_quarto.modules.constants.kDataCodeCellAnnotation, annotationToken} } definition = pandoc.Span(definitionContent, pandoc.Attr(attribs)) - else + else definition = pandoc.Plain(definitionContent) end @@ -560,6 +701,7 @@ function code_annotations() else flushPending() end + end -- end generic handler else outputBlockClearPending(block) end diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index 40dc1a164b5..4015092cc8e 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -24,12 +24,101 @@ // 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, color: none) = context { + let c = if color != none { color } else { text.fill } + box(baseline: 15%, circle( + radius: 0.55em, + stroke: 0.5pt + c, + )[#set text(size: 0.7em, fill: c); #align(center + horizon, str(n))]) +} + +// Link to `target` only if that label exists in the document; otherwise +// render `body` unlinked. Keeps annotation rendering resilient when the +// counterpart (annotation item or code line) is missing. +#let quarto-annote-link(target, body) = context { + if query(label(target)).len() > 0 { link(label(target), body) } else { body } +} + +// Derive a contrasting annotation colour from a background fill. +// Light backgrounds get dark circles; dark backgrounds get light circles. +#let quarto-annote-color(bg) = { + if type(bg) == color { + 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) + } +} + +#let quarto-code-filename(filename, body) = { + show raw.where(block: true): it => it + block(width: 100%, radius: 2pt, clip: true)[ + #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, 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) + let is-first = first-lines.at(str(annote-num), default: none) == str(it.number) + if is-first { + box(width: 100%)[#it #h(1fr) #quarto-annote-link(lbl)[#quarto-circled-number(annote-num, color: color)] #label(lbl + "-back")] + } else { + box(width: 100%)[#it #h(1fr) #quarto-annote-link(lbl)[#quarto-circled-number(annote-num, color: color)]] + } + } else { + box(width: 100%)[#it #h(1fr) #quarto-circled-number(annote-num, color: color)] + } + } else { + it + } + } + body +} + +#let quarto-annotation-item(cell-id, n, content) = { + if cell-id != "" { + [#block(above: 0.4em, below: 0.4em)[ + #quarto-annote-link(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 and radius +#show raw.where(block: true): it => block( + fill: luma(230), width: 100%, inset: 8pt, radius: 2pt, 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-backtick-fence.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-backtick-fence.qmd new file mode 100644 index 00000000000..133763c8042 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-backtick-fence.qmd @@ -0,0 +1,22 @@ +--- +title: Code Annotations (code containing backtick runs) +format: + typst: + keep-typ: true + syntax-highlighting: idiomatic + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + # Fence must grow beyond the longest backtick run in the code + - '#quarto-code-annotation\(\("1": 1\), cell-id: "annotated-cell-1"\)\[```````python' + - [] +--- + +```{.python} +s = "``````" # <1> +``` + +1. Backticks in string. 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 00000000000..609c41dfc9a --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-jupyter.qmd @@ -0,0 +1,26 @@ +--- +title: Code Annotations (Cell, Jupyter) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - "quarto-circled-number" + - "quarto-annotation-item" + - "cell-id:" + - "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 00000000000..f2883a45b60 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-cell-knitr.qmd @@ -0,0 +1,26 @@ +--- +title: Code Annotations (Cell, Knitr) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - "quarto-circled-number" + - "quarto-annotation-item" + - "cell-id:" + - "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-crossref-listing-cell-knitr.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing-cell-knitr.qmd new file mode 100644 index 00000000000..663bde0b410 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing-cell-knitr.qmd @@ -0,0 +1,27 @@ +--- +title: Code Annotations (crossref listing from executable cell) +format: + typst: + keep-typ: true + code-annotations: true +engine: knitr +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'Skylighting\(cell-id: "annotated-cell' + - 'quarto-annotation-item\("annotated-cell' + - + - 'quarto-code-annotations:' +--- + +```{r} +#| lst-label: lst-hello +#| lst-cap: "A listing" +print("Hello") # <1> +``` + +1. Print a greeting. + +See @lst-hello. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing-idiomatic.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing-idiomatic.qmd new file mode 100644 index 00000000000..1ae4016193a --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing-idiomatic.qmd @@ -0,0 +1,24 @@ +--- +title: Code Annotations (crossref listing, idiomatic) +format: + typst: + keep-typ: true + syntax-highlighting: idiomatic + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - '#quarto-code-annotation\(' + - '#quarto-annotation-item\("annotated-cell' + - [] +--- + +```{#lst-good .python lst-cap="A listing"} +x = 1 # <1> +``` + +1. Assign x. + +See @lst-good. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing.qmd new file mode 100644 index 00000000000..0520563f0f3 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-crossref-listing.qmd @@ -0,0 +1,24 @@ +--- +title: Code Annotations (crossref listing) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'Skylighting\(cell-id: "annotated-cell' + - 'quarto-annotation-item\("annotated-cell' + - + - 'quarto-code-annotations:' +--- + +```{#lst-good .python lst-cap="A listing"} +x = 1 # <1> +``` + +1. Assign x. + +See @lst-good. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-fenced-echo-idiomatic.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-fenced-echo-idiomatic.qmd new file mode 100644 index 00000000000..02f32096cea --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-fenced-echo-idiomatic.qmd @@ -0,0 +1,23 @@ +--- +title: Code Annotations (fenced echo, idiomatic) +format: + typst: + keep-typ: true + syntax-highlighting: idiomatic + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + # Wrapped raw content has no fence line, so the key stays 1 + - '#quarto-code-annotation\(\("1": 1\), cell-id: "annotated-cell-1"\)' + - '#quarto-annotation-item\("annotated-cell-1"' + - [] +--- + +```{{r}} +x <- 1 # <1> +``` + +1. Assign x. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-fenced-echo.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-fenced-echo.qmd new file mode 100644 index 00000000000..cc573696dbd --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-fenced-echo.qmd @@ -0,0 +1,24 @@ +--- +title: Code Annotations (fenced echo display block) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + # Skylighting output keeps the injected fence line, so the + # annotated code line is physical line 2 + - 'Skylighting\(cell-id: "annotated-cell-1", annotations: \("2": 1\)' + - '#quarto-annotation-item\("annotated-cell-1"' + - + - 'quarto-code-annotations:' +--- + +```{{r}} +x <- 1 # <1> +``` + +1. Assign x. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-highlight-none.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-highlight-none.qmd new file mode 100644 index 00000000000..9751ef1b54e --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-highlight-none.qmd @@ -0,0 +1,23 @@ +--- +title: Code Annotations (syntax-highlighting none) +format: + typst: + keep-typ: true + syntax-highlighting: none + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - '#quarto-code-annotation\(\("1": 1\), cell-id: "annotated-cell-1"\)' + - '#quarto-annotation-item\("annotated-cell-1"' + - + - '#Skylighting\(cell-id:' +--- + +```{.python} +x = 1 # <1> +``` + +1. Assign x. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-light-dark-theme.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-light-dark-theme.qmd new file mode 100644 index 00000000000..90ad8361a0a --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-light-dark-theme.qmd @@ -0,0 +1,25 @@ +--- +title: Code Annotations (light/dark highlighting object) +format: + typst: + keep-typ: true + code-annotations: true +syntax-highlighting: + light: github + dark: github-dark +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'Skylighting\(cell-id: "annotated-cell' + - 'quarto-annotation-item\("annotated-cell' + - + - '#quarto-code-annotation\(' +--- + +```{.python} +x = 1 # <1> +``` + +1. Assign x. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-listing-id.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-listing-id.qmd new file mode 100644 index 00000000000..f5eca13121f --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-listing-id.qmd @@ -0,0 +1,22 @@ +--- +title: Code Annotations (identifier with dot) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'Skylighting\(cell-id: "lst-a\.b"' + - 'quarto-annotation-item\("lst-a\.b"' + - + - 'quarto-code-annotations:' +--- + +```{#lst-a.b .python} +x = 1 # <1> +``` + +1. Assign x. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-multiline.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-multiline.qmd new file mode 100644 index 00000000000..46e8a77ecb8 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-multiline.qmd @@ -0,0 +1,26 @@ +--- +title: Code Annotations (one annotation spanning multiple lines) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + # Annotation 1 spans lines 1-2; back-label dedup must keep this compiling + - 'Skylighting\(cell-id: "annotated-cell-1", annotations: \("1": 1, "2": 1, "3": 2\)' + - '#quarto-annotation-item\("annotated-cell-1", 1' + - '#quarto-annotation-item\("annotated-cell-1", 2' + - [] +--- + +```{.python} +x = 1 # <1> +y = 2 # <1> +z = 3 # <2> +``` + +1. Assign. +2. Last. 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 00000000000..2cf8523343b --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-native.qmd @@ -0,0 +1,26 @@ +--- +title: Code Annotations (Native) +format: + typst: + keep-typ: true + code-annotations: true + syntax-highlighting: idiomatic +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + # Call sites, not the helper definitions inlined from definitions.typ + - '#quarto-code-annotation\(\("1": 1, "3": 2\), cell-id: "annotated-cell-1"\)' + - '#quarto-annotation-item\("annotated-cell-1", 1' + - [] +--- + +```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-no-list.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-no-list.qmd new file mode 100644 index 00000000000..e611f763dc1 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-no-list.qmd @@ -0,0 +1,21 @@ +--- +title: Code Annotations (no annotation list) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'annotations: \("1": 1\)' + - + - 'quarto-annotation-item\("' +--- + +```{.python} +x = 1 # <1> +``` + +Prose without an ordered list; the circle renders unlinked. 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 00000000000..d5b39ac7358 --- /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\(' + - '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-startfrom-number-lines.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-startfrom-number-lines.qmd new file mode 100644 index 00000000000..7f2506736bd --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-startfrom-number-lines.qmd @@ -0,0 +1,21 @@ +--- +title: Code Annotations (startFrom with line numbers) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'annotations: \("1": 1\)' + - 'start: 5' + - [] +--- + +```{.python .number-lines startFrom="5"} +x = 1 # <1> +``` + +1. Assign x. diff --git a/tests/docs/smoke-all/typst/code-annotations/code-annotations-startfrom.qmd b/tests/docs/smoke-all/typst/code-annotations/code-annotations-startfrom.qmd new file mode 100644 index 00000000000..0fad9a3902b --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations-startfrom.qmd @@ -0,0 +1,21 @@ +--- +title: Code Annotations (startFrom without line numbers) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'annotations: \("1": 1\)' + - 'quarto-annotation-item\(' + - [] +--- + +```{.python startFrom="5"} +x = 1 # <1> +``` + +1. Assign x. 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 00000000000..927c58d8f19 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-annotations/code-annotations.qmd @@ -0,0 +1,26 @@ +--- +title: Code Annotations (Skylighting) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + # Call sites, not the helper definitions inlined from definitions.typ + - 'Skylighting\(cell-id: "annotated-cell-1", annotations: \("1": 1, "3": 2\)' + - '#quarto-annotation-item\("annotated-cell-1", 1' + - '#quarto-annotation-item\("annotated-cell-1", 2' + - [] +--- + +```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-annotation-cell-jupyter.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd new file mode 100644 index 00000000000..25add896637 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-jupyter.qmd @@ -0,0 +1,24 @@ +--- +title: Code Filename + Annotation (Cell, Jupyter) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\("hello\.py"\)\[\s*#Skylighting\(cell-id:' + - "quarto-annotation-item" + - "cell-id:" + - "annotations:" + - [] +--- + +```{python} +#| filename: "hello.py" +print("Hello, world!") # <1> +``` + +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 new file mode 100644 index 00000000000..76372384a9d --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-cell-knitr.qmd @@ -0,0 +1,24 @@ +--- +title: Code Filename + Annotation (Cell, Knitr) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\("hello\.R"\)\[\s*#Skylighting\(cell-id:' + - "quarto-annotation-item" + - "cell-id:" + - "annotations:" + - [] +--- + +```{r} +#| filename: "hello.R" +print("Hello, world!") # <1> +``` + +1. Print a greeting. 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 00000000000..2789e01631d --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation-native.qmd @@ -0,0 +1,27 @@ +--- +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" + - "cell-id:" + - "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 00000000000..2182af5069d --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-annotation.qmd @@ -0,0 +1,26 @@ +--- +title: Code Filename + Annotation (Code Block) +format: + typst: + keep-typ: true + code-annotations: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - "quarto-annotation-item" + - "cell-id:" + - "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 00000000000..fc839eef4f9 --- /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 00000000000..867209f7512 --- /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-escape.qmd b/tests/docs/smoke-all/typst/code-filename/code-filename-escape.qmd new file mode 100644 index 00000000000..811caa8c0c1 --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename-escape.qmd @@ -0,0 +1,17 @@ +--- +title: Code Filename (characters needing escaping) +format: + typst: + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - '#quarto-code-filename\("C:\\\\code\\\\hello\.R"\)\[' + - [] +--- + +```{.r filename="C:\code\hello.R"} +x <- 1 +``` 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 00000000000..5f6fa3caf8e --- /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 new file mode 100644 index 00000000000..db7f158771b --- /dev/null +++ b/tests/docs/smoke-all/typst/code-filename/code-filename.qmd @@ -0,0 +1,18 @@ +--- +title: Code Filename (Code Block) +format: + typst: + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'quarto-code-filename\(' + - [] +--- + +```{.python filename="example.py"} +def hello(): + print("Hello, world!") +``` 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 de81bcf6460..5160ed005fb 100644 --- a/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-default.qmd +++ b/tests/docs/smoke-all/typst/syntax-highlighting/skylighting-default.qmd @@ -15,7 +15,10 @@ _quarto: # Quarto override with proper block styling and arrow theme bgcolor - 'let bgcolor = rgb\("#f1f3f5"\)' - 'block\(fill: bgcolor, width: 100%, inset: 8pt, radius: 2pt, blocks\)' - - ["```python"] + - + - "```python" + # Stroke was reverted for consistency with other formats + - 'stroke: 0\.5pt \+ luma\(200\)' --- Default Skylighting (no explicit theme, no brand). This tests the baseline