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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All changes included in 1.10:
### `typst`

- ([#14261](https://github.com/quarto-dev/quarto-cli/issues/14261)): Fix theorem/example block titles containing inline code producing invalid Typst markup when syntax highlighting is applied.
- ([#14290](https://github.com/quarto-dev/quarto-cli/issues/14290)): Fix cross-referencing `remark` and `solution` environments producing invalid Typst output.

## Commands

Expand Down
54 changes: 53 additions & 1 deletion src/resources/filters/customnodes/proof.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,37 @@ _quarto.ast.add_handler({
end
})

-- Color mapping for clouds/rainbow themes (per proof type)
local proof_theme_colors = {
proof = "gray", remark = "orange", solution = "teal"
}

local function ensure_typst_proofs(proof_env)
local appearance = _quarto.modules.theorems.ensure_appearance_imports()
local proof_info = proof_types[proof_env]
local title = envTitle(proof_env, proof_info.title)
local render_code

if appearance == "fancy" then
render_code = " render: fancy-box.with(\n" ..
" get-border-color: get-tertiary-border-color,\n" ..
" get-body-color: get-tertiary-body-color,\n" ..
" get-symbol: loc => none,\n" ..
" ),\n"
elseif appearance == "clouds" then
local color = proof_theme_colors[proof_env] or "gray"
render_code = " render: clouds-render.with(fill: " .. color .. ".lighten(85%)),\n"
elseif appearance == "rainbow" then
local color = proof_theme_colors[proof_env] or "gray"
render_code = " render: rainbow-render.with(fill: " .. color .. ".darken(20%)),\n"
else
_quarto.modules.theorems.ensure_simple_render("simple-proof-render", false)
render_code = " render: simple-proof-render,\n"
end

_quarto.modules.theorems.ensure_frame(proof_env, title, render_code)
end

function is_proof_div(div)
local ref = refType(div.identifier)
if ref ~= nil then
Expand Down Expand Up @@ -141,6 +172,27 @@ end, function(proof_tbl)
end
elseif _quarto.format.isJatsOutput() then
el = jatsTheorem(el, nil, name )
elseif _quarto.format.isTypstOutput() then
if #el.content == 0 then
warn("Proof block has no content; skipping")
return pandoc.Null()
end
ensure_typst_proofs(proof.env)
local preamble = pandoc.Plain({pandoc.RawInline("typst", "#" .. proof.env .. "(")})
if name and #name > 0 then
preamble.content:insert(pandoc.RawInline("typst", 'title: ['))
tappend(preamble.content, name)
preamble.content:insert(pandoc.RawInline("typst", ']'))
end
preamble.content:insert(pandoc.RawInline("typst", ")["))
local callproof = make_scaffold(pandoc.Div, preamble)
tappend(callproof.content, quarto.utils.as_blocks(el.content))
if proof_tbl.identifier and proof_tbl.identifier ~= "" then
callproof.content:insert(pandoc.RawInline("typst", "] <" .. proof_tbl.identifier .. ">"))
else
callproof.content:insert(pandoc.RawInline("typst", "]"))
end
return callproof
else
el.classes:insert(proof.title:lower())
local span_title = pandoc.Emph(pandoc.Str(envTitle(proof.env, proof.title)))
Expand Down Expand Up @@ -176,4 +228,4 @@ end, function(proof_tbl)

return el

end)
end)
125 changes: 26 additions & 99 deletions src/resources/filters/customnodes/theorem.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,115 +93,42 @@ _quarto.ast.add_handler({
end
})

-- Get theorem-appearance option (simple, fancy, clouds, rainbow)
local function get_theorem_appearance()
local appearance = option("theorem-appearance", "simple")
if appearance ~= nil and type(appearance) == "table" then
appearance = pandoc.utils.stringify(appearance)
end
return appearance or "simple"
end

-- Color mapping for clouds/rainbow themes (per theorem type)
local theme_colors = {
thm = "red", lem = "teal", cor = "navy", prp = "blue",
cnj = "navy", def = "olive", exm = "green", exr = "purple", alg = "maroon"
}

local included_typst_theorems = false
local letted_typst_theorem = {}
local function ensure_typst_theorems(reftype)
local appearance = get_theorem_appearance()

if not included_typst_theorems then
included_typst_theorems = true

if appearance == "fancy" then
-- Import theorion's make-frame and fancy-box theming
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame, cosmos
#import cosmos.fancy: fancy-box, set-primary-border-color, set-primary-body-color, set-secondary-border-color, set-secondary-body-color, set-tertiary-border-color, set-tertiary-body-color, get-primary-border-color, get-primary-body-color, get-secondary-border-color, get-secondary-body-color, get-tertiary-border-color, get-tertiary-body-color
]])
-- Set theorem colors from brand-color (runs in before-body, after brand-color is defined)
quarto.doc.include_text("before-body", [[
#set-primary-border-color(brand-color.at("primary", default: green.darken(30%)))
#set-primary-body-color(brand-color.at("primary", default: green).lighten(90%))
#set-secondary-border-color(brand-color.at("secondary", default: orange))
#set-secondary-body-color(brand-color.at("secondary", default: orange).lighten(90%))
#set-tertiary-border-color(brand-color.at("tertiary", default: blue.darken(30%)))
#set-tertiary-body-color(brand-color.at("tertiary", default: blue).lighten(90%))
]])
elseif appearance == "clouds" then
-- Import theorion's make-frame and clouds render function
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame, cosmos
#import cosmos.clouds: render-fn as clouds-render
]])
elseif appearance == "rainbow" then
-- Import theorion's make-frame and rainbow render function
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame, cosmos
#import cosmos.rainbow: render-fn as rainbow-render
]])
else -- simple (default)
-- Import only make-frame and define simple render function
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame

// Simple theorem render: bold title with period, italic body
#let simple-theorem-render(prefix: none, title: "", full-title: auto, body) = {
if full-title != "" and full-title != auto and full-title != none {
strong[#full-title.]
h(0.5em)
}
emph(body)
parbreak()
}
]])
local appearance = _quarto.modules.theorems.ensure_appearance_imports()
local theorem_type = theorem_types[reftype]
local title = titleString(reftype, theorem_type.title)
local render_code

if appearance == "fancy" then
local color_scheme = "secondary"
if theorem_type.style == "definition" then
color_scheme = "primary"
elseif reftype == "prp" then
color_scheme = "tertiary"
end
render_code = " render: fancy-box.with(\n" ..
" get-border-color: get-" .. color_scheme .. "-border-color,\n" ..
" get-body-color: get-" .. color_scheme .. "-body-color,\n" ..
" get-symbol: loc => none,\n" ..
" ),\n"
elseif appearance == "clouds" then
local color = theme_colors[reftype] or "gray"
render_code = " render: clouds-render.with(fill: " .. color .. ".lighten(85%)),\n"
elseif appearance == "rainbow" then
local color = theme_colors[reftype] or "gray"
render_code = " render: rainbow-render.with(fill: " .. color .. ".darken(20%)),\n"
else
_quarto.modules.theorems.ensure_simple_render("simple-theorem-render", true)
render_code = " render: simple-theorem-render,\n"
end

if not letted_typst_theorem[reftype] then
letted_typst_theorem[reftype] = true
local theorem_type = theorem_types[reftype]
local title = titleString(reftype, theorem_type.title)

-- Build render code based on appearance
local render_code
if appearance == "fancy" then
-- Map theorem styles to color schemes (primary=definitions, secondary=theorems, tertiary=propositions)
local color_scheme = "secondary" -- default for most theorem types
if theorem_type.style == "definition" then
color_scheme = "primary"
elseif reftype == "prp" then
color_scheme = "tertiary"
end
render_code = " render: fancy-box.with(\n" ..
" get-border-color: get-" .. color_scheme .. "-border-color,\n" ..
" get-body-color: get-" .. color_scheme .. "-body-color,\n" ..
" get-symbol: loc => none,\n" ..
" ),\n"
elseif appearance == "clouds" then
local color = theme_colors[reftype] or "gray"
render_code = " render: clouds-render.with(fill: " .. color .. ".lighten(85%)),\n"
elseif appearance == "rainbow" then
local color = theme_colors[reftype] or "gray"
render_code = " render: rainbow-render.with(fill: " .. color .. ".darken(20%)),\n"
else -- simple
render_code = " render: simple-theorem-render,\n"
end

-- Use theorion's make-frame with appropriate render
quarto.doc.include_text("in-header", "#let (" .. theorem_type.env .. "-counter, " .. theorem_type.env .. "-box, " ..
theorem_type.env .. ", show-" .. theorem_type.env .. ") = make-frame(\n" ..
" \"" .. theorem_type.env .. "\",\n" ..
" text(weight: \"bold\")[" .. title .. "],\n" ..
" inherited-levels: theorem-inherited-levels,\n" ..
" numbering: theorem-numbering,\n" ..
render_code ..
")")
quarto.doc.include_text("in-header", "#show: show-" .. theorem_type.env)
end
_quarto.modules.theorems.ensure_frame(theorem_type.env, title, render_code)
end


Expand Down
1 change: 1 addition & 0 deletions src/resources/filters/modules/import_all.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ _quarto.modules = {
scope = require("modules/scope"),
string = require("modules/string"),
tablecolwidths = require("modules/tablecolwidths"),
theorems = require("modules/theorems"),
typst = require("modules/typst"),
listtable = require("modules/listtable"),
tableutils = require("modules/tableutils"),
Expand Down
101 changes: 101 additions & 0 deletions src/resources/filters/modules/theorems.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
-- theorems.lua
-- Shared Typst theorem/proof setup logic using theorion
-- Copyright (C) 2020-2022 Posit Software, PBC

local typst_theorem_appearance_imported = false
local typst_theorem_like_frames = {}
local typst_simple_renderers = {}

local function appearance()
local val = option("theorem-appearance", "simple")
if type(val) == "table" then
val = pandoc.utils.stringify(val)
end
return val or "simple"
end

local function ensure_appearance_imports()
local val = appearance()
if typst_theorem_appearance_imported then
return val
end

typst_theorem_appearance_imported = true
if val == "fancy" then
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame, cosmos
#import cosmos.fancy: fancy-box, set-primary-border-color, set-primary-body-color, set-secondary-border-color, set-secondary-body-color, set-tertiary-border-color, set-tertiary-body-color, get-primary-border-color, get-primary-body-color, get-secondary-border-color, get-secondary-body-color, get-tertiary-border-color, get-tertiary-body-color
]])
quarto.doc.include_text("before-body", [[
#set-primary-border-color(brand-color.at("primary", default: green.darken(30%)))
#set-primary-body-color(brand-color.at("primary", default: green).lighten(90%))
#set-secondary-border-color(brand-color.at("secondary", default: orange))
#set-secondary-body-color(brand-color.at("secondary", default: orange).lighten(90%))
#set-tertiary-border-color(brand-color.at("tertiary", default: blue.darken(30%)))
#set-tertiary-body-color(brand-color.at("tertiary", default: blue).lighten(90%))
]])
elseif val == "clouds" then
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame, cosmos
#import cosmos.clouds: render-fn as clouds-render
]])
elseif val == "rainbow" then
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame, cosmos
#import cosmos.rainbow: render-fn as rainbow-render
]])
else
quarto.doc.include_text("in-header", [[
#import "@preview/theorion:0.4.1": make-frame
]])
end

return val
end

local function ensure_simple_render(render_name, italic_body)
if typst_simple_renderers[render_name] then
return
end

typst_simple_renderers[render_name] = true
local body_render = "body"
if italic_body then
body_render = "emph(body)"
end

quarto.doc.include_text("in-header", "#let " .. render_name .. [[(prefix: none, title: "", full-title: auto, body) = {
if full-title != "" and full-title != auto and full-title != none {
strong[#full-title.]
h(0.5em)
}
]] .. body_render .. "\n" .. [[
parbreak()
}
]])
end

local function ensure_frame(env_name, title, render_code)
if typst_theorem_like_frames[env_name] then
return false
end

typst_theorem_like_frames[env_name] = true
quarto.doc.include_text("in-header", "#let (" .. env_name .. "-counter, " .. env_name .. "-box, " ..
env_name .. ", show-" .. env_name .. ") = make-frame(\n" ..
" \"" .. env_name .. "\",\n" ..
" text(weight: \"bold\")[" .. title .. "],\n" ..
" inherited-levels: theorem-inherited-levels,\n" ..
" numbering: theorem-numbering,\n" ..
render_code ..
")")
quarto.doc.include_text("in-header", "#show: show-" .. env_name)
return true
end

return {
appearance = appearance,
ensure_appearance_imports = ensure_appearance_imports,
ensure_simple_render = ensure_simple_render,
ensure_frame = ensure_frame,
}
3 changes: 2 additions & 1 deletion tests/docs/smoke-all/2026/02/04/issue-13992-proof.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ _quarto:
- []
- ['Proof content visible']
typst:
noErrors: default
ensureTypstFileRegexMatches:
- ['#emph\[Proof\]\. Proof content visible']
- ['#proof\(']
- []
---

Expand Down
43 changes: 43 additions & 0 deletions tests/docs/smoke-all/2026/04/04/issue-14290.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: "Cross-reference remark and solution in Typst (#14290)"
keep-typ: true
_quarto:
tests:
typst:
noErrors: default
ensureTypstFileRegexMatches:
-
- '#remark\('
- '#solution\('
- '<rem-example>'
- '<sol-exercise>'
- '#show: show-remark'
- '#show: show-solution'
- '#ref\(<rem-example>'
- '#ref\(<sol-exercise>'
- []
---

::: {#rem-example}

## A Remark

This is a remark that should be cross-referenceable.

:::

::: {#sol-exercise}

## A Solution

This is a solution that should be cross-referenceable.

:::

::: {.proof}

This is an unnumbered proof.

:::

See @rem-example and @sol-exercise.
Loading