My extension: typst orange book added

This commit is contained in:
2026-03-02 14:42:08 +01:00
parent 68983e30b1
commit 457873a31b
8 changed files with 335 additions and 11 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Posit Software, PBC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +1,7 @@
title: Julia-color
author: Meik Hellmund
version: 1.0.0
quarto-required: ">=99.9.0"
quarto-required: ">=1.9.0"
contributes:
formats:
common:
@@ -14,8 +14,18 @@ contributes:
- "resources/css/juliamono.css"
pdf:
include-in-header:
- "resources/tex/juliainc.tex"
- "resources/tex/juliainc.tex"
typst:
include-in-header:
- "resources/typst/juliainc.typ"
- "resources/typst/juliainc.typ"
# the following is from
# quarto-cli/src/resources/extension-subtrees/orange-book/_extensions/orange-book
# comment out for an article
template-partials:
- numbering.typ
- page.typ
- typst-show.typ
filters:
- path: orange-book.lua
at: post-quarto

View File

@@ -1,6 +1,8 @@
-- based on
-- https://github.com/jupyter/nbconvert/blob/main/nbconvert/filters/ansi.py
-- debug: pandoc -t native file.{typst,html,..}.md
local ANSI_COLORS = {
"ansi-black",
"ansi-red",
@@ -224,18 +226,47 @@ local function HTMLconverter(fg, bg, bold, light, italic, underline, inverse)
return starttag..">","</span>"
end
local function escapeTypstString(s)
s = s:gsub("\\", "\\\\")
s = s:gsub("%$", "\\$")
s = s:gsub("*", "\\*")
s = s:gsub([["]],[[\"]])
return s
end
local function escapeTypstString2(s)
s = s:gsub("@", "\\@")
s = s:gsub("<", "\\<")
s = s:gsub("#", "\\#")
s = s:gsub(">", "\\>")
--s = s:gsub("]","\\]")
s = s:gsub("\n","\\\n")
s = s:gsub("+","\\+")
s = s:gsub("/","\\/")
s = s:gsub("=","\\=")
return s
end
local function noescapeString(s)
return s
end
local function codeBlockTrans(e)
local converter, fmt
local converter, fmt, escapeString
if quarto.doc.isFormat('latex') then
converter = LaTeXconverter
fmt = 'latex'
escapeString = noescapeString
elseif quarto.doc.isFormat('html') then
converter = HTMLconverter
fmt = 'html'
escapeString = noescapeString
elseif quarto.doc.isFormat('typst') then
converter = Typstconverter
fmt = 'typst'
escapeString = escapeTypstString
else
return
end
@@ -264,7 +295,7 @@ local function codeBlockTrans(e)
local out=""
local text = e.text
-- we remove links (eg in julia ParseErrors. THey link to local files, so they are useless anyway)
-- we remove links (eg in julia ParseErrors. They link to local files, so they are useless anyway)
text = text:gsub("\x1b%]8;.-\x1b\\", "")
if string.find(text, "\x1b%[") then
@@ -305,7 +336,7 @@ local function codeBlockTrans(e)
else
starttag, endtag = converter(fg, bg, bold, light, italic, underline, inverse)
end
out = out .. starttag .. chunk .. endtag
out = out .. starttag .. escapeString(chunk) .. endtag
end
while next(numbers) ~= nil do
@@ -359,7 +390,7 @@ local function codeBlockTrans(e)
end
end
else
out = text
out = escapeString(text)
end
if fmt == 'html' then
return pandoc.RawBlock(fmt,
@@ -369,7 +400,11 @@ local function codeBlockTrans(e)
return pandoc.RawBlock(fmt, [[\begin{]]..texenv.."}\n"..out.."\n"..[[\end{]].. texenv .. "}")
end
if fmt == 'typst' then
return pandoc.RawBlock(fmt, "#"..texenv.."[\n"..out.."\n]")
if texenv == "OutputCell" then
return pandoc.RawBlock(fmt, "#"..texenv.."[\n" .. escapeTypstString2(out) .. "\n]")
else
return pandoc.RawBlock(fmt, "#"..texenv.."[\n" .. out .. "\n]")
end
end
@@ -398,12 +433,52 @@ local function divCodeBlockNoHeader1(e)
if el.t == 'Header' then
el.level = 6
end
-- if el.t == 'CodeBlock' then -- attempt for typst, but we use an extra quarto cell
-- for i,v in ipairs(el.classes) do
-- if v=="julia" or v=="jldoctest" then -- example code and before
-- el.classes:remove(i)
-- end
-- end
-- end
end
return e
end
-- test if two divs should be merged
local function testmerge(d1, d2)
return d1 and d1.t == "Div" and d1.classes:includes("cell-output") and #d1.content == 1
and d2 and d2.t == "Div" and d2.classes:includes("cell-output") and #d2.content == 1
and d1.content[1].t == "CodeBlock" and not d1.classes:includes("cell-output-stderr")
and d2.content[1].t == "CodeBlock" and not d2.classes:includes("cell-output-stderr")
end
-- merge (div (codecell (text1)), div (codecell(text2))) to div(codecell(text1+text2))
local function blockMerge(es)
local nl = ""
for i = #es-1, 1, -1 do
if testmerge(es[i], es[i+1]) then
str1 = es[i].content[1].text
str2 = es[i+1].content[1].text
nl = "\n"
if es[i].classes:includes("cell-output-stdout") and es[i+1].classes:includes("cell-output-stdout") then
if str1:sub(-1) == "\n" then
nl = ""
end
if str2:sub(1, 1) == "\n" then
nl = ""
end
end
es[i].content[1].text = str1 .. nl .. str2
es:remove(i+1)
end
end
return es
end
return {
{Div = divStderr},
{Div = divCodeBlockNoHeader1},
{Blocks = blockMerge},
{CodeBlock = codeBlockTrans},
}

View File

@@ -0,0 +1,38 @@
// Chapter-based numbering for books with appendix support
#let equation-numbering = it => {
let pattern = if state("appendix-state", none).get() != none { "(A.1)" } else { "(1.1)" }
numbering(pattern, counter(heading).get().first(), it)
}
#let callout-numbering = it => {
let pattern = if state("appendix-state", none).get() != none { "A.1" } else { "1.1" }
numbering(pattern, counter(heading).get().first(), it)
}
#let subfloat-numbering(n-super, subfloat-idx) = {
let chapter = counter(heading).get().first()
let pattern = if state("appendix-state", none).get() != none { "A.1a" } else { "1.1a" }
numbering(pattern, chapter, n-super, subfloat-idx)
}
// Theorem configuration for theorion
// Chapter-based numbering (H1 = chapters)
#let theorem-inherited-levels = 1
// Appendix-aware theorem numbering
#let theorem-numbering(loc) = {
if state("appendix-state", none).at(loc) != none { "A.1" } else { "1.1" }
}
// Theorem render function
// Note: brand-color is not available at this point in template processing
#let theorem-render(prefix: none, title: "", full-title: auto, body) = {
block(
width: 100%,
inset: (left: 1em),
stroke: (left: 2pt + black),
)[
#if full-title != "" and full-title != auto and full-title != none {
strong[#full-title]
linebreak()
}
#body
]
}

View File

@@ -0,0 +1,69 @@
-- orange-book.lua
-- Orange-book specific part and appendix handling for Typst books
local function is_typst_book()
local file_state = quarto.doc.file_metadata()
return quarto.doc.is_format("typst") and
file_state ~= nil and
file_state.file ~= nil
end
local header_filter = {
Header = function(el)
local file_state = quarto.doc.file_metadata()
if not is_typst_book() then
return nil
end
if file_state == nil or file_state.file == nil then
return nil
end
local file = file_state.file
local bookItemType = file.bookItemType
if el.level ~= 1 or bookItemType == nil then
return nil
end
-- Handle parts
if bookItemType == "part" then
return pandoc.RawBlock('typst', '#part[' .. pandoc.utils.stringify(el.content) .. ']')
end
-- Handle appendices
if bookItemType == "appendix" then
-- First appendix triggers the show rule with localized "Appendices" title
if file.bookItemNumber == 1 or file.bookItemNumber == nil then
-- Get localized title from language settings
local language = quarto.doc.language
local appendicesTitle = (language and language["section-title-appendices"]) or "Appendices"
-- Use hide-parent: true to work around orange-book bug where unnumbered headings
-- (like Bibliography) trigger duplicate "Appendices" TOC entries.
local appendixStart = pandoc.RawBlock('typst',
'#show: appendices.with("' .. appendicesTitle .. '", hide-parent: true)')
-- If this is the synthetic "Appendices" divider heading (has .unnumbered class),
-- emit our own Appendices heading for TOC display
if el.classes:includes("unnumbered") then
local appendicesHeading = pandoc.RawBlock('typst',
'#heading(level: 1, numbering: none)[' .. appendicesTitle .. ']')
return {appendixStart, appendicesHeading}
end
return {appendixStart, el}
end
end
return nil
end
}
-- Combine with file_metadata_filter so book metadata markers are parsed
-- during this filter's document traversal (needed for bookItemType, etc.)
return quarto.utils.combineFilters({
quarto.utils.file_metadata_filter(),
header_filter
})

View File

@@ -0,0 +1,15 @@
#set page(
paper: $if(papersize)$"$papersize$"$else$"us-letter"$endif$,
$if(margin-geometry)$
// Margins handled by marginalia.setup in typst-show.typ AFTER book.with()
$elseif(margin)$
margin: ($for(margin/pairs)$$margin.key$: $margin.value$,$endfor$),
$else$
margin: (x: 1.25in, y: 1.25in),
$endif$
numbering: $if(page-numbering)$"$page-numbering$"$else$none$endif$,
columns: $if(columns)$$columns$$else$1$endif$,
)
// Logo is handled by orange-book's cover page, not as a page background
// NOTE: marginalia.setup is called in typst-show.typ AFTER book.with()
// to ensure marginalia's margins override the book format's default margins

View File

@@ -2,10 +2,45 @@
#show raw: set text(font: "JuliaMono")
// define cell layout
#let OutputCell = block.with(width:100%, inset: 5pt)
#let AnsiOutputCell = block.with(width: 100%, inset: 5pt)
//#let EndLine() = raw("\n")
//#let OutputCell(lines) = {
// let blocks = []
// for ln in lines {
// blocks = blocks + ln + EndLine()
// }
// block(width:100%, inset:(x:5pt, y:0pt), stroke:( left: 2pt + gray), blocks)
//}
#let OutputCell(content) = block(
width: 100%,
inset: (x: 5pt, y: 5pt),
above: 0pt,
below: 8pt,
stroke: (left: 2pt + gray, bottom: .5pt + gray)
)[
#set text(font: "JuliaMono", size:8pt)
#content
]
#let AnsiOutputCell(content) = block(
width: 100%,
inset: (x: 5pt, y: 5pt),
above: 0pt,
below: 8pt,
stroke: (left: 2pt + gray, bottom: .5pt + gray)
)[
#set text(font: "JuliaMono", size:9pt)
#content
]
// does not exist with julia engine
#let StderrOutputCell = block.with(width: 100%, stroke: 1pt + red, inset: 5pt)
//#set highlight(top-edge: "ascender", bottom-edge: "descender")
#let invertbox(color, c) = box(outset: (x: 0.05em, y: 0.25em), fill: color, c)

View File

@@ -0,0 +1,61 @@
#import "@preview/orange-book:0.7.1": book, part, chapter, appendices
#show: book.with(
$if(title)$
title: [$title$],
$endif$
$if(subtitle)$
subtitle: [$subtitle$],
$endif$
$if(by-author)$
author: "$for(by-author)$$it.name.literal$$sep$, $endfor$",
$endif$
$if(date)$
date: "$date$",
$endif$
$if(lang)$
lang: "$lang$",
$endif$
main-color: brand-color.at("primary", default: blue),
logo: {
let logo-info = brand-logo.at("medium", default: none)
if logo-info != none { image(logo-info.path, alt: logo-info.at("alt", default: none)) }
},
$if(toc-depth)$
outline-depth: $toc-depth$,
$endif$
$if(lof)$
list-of-figure-title: "$if(crossref.lof-title)$$crossref.lof-title$$else$$crossref-lof-title$$endif$",
$endif$
$if(lot)$
list-of-table-title: "$if(crossref.lot-title)$$crossref.lot-title$$else$$crossref-lot-title$$endif$",
$endif$
$if(margin-geometry)$
padded-heading-number: false,
$endif$
)
$if(margin-geometry)$
// Configure marginalia page geometry for book context
// Geometry computed by Quarto's meta.lua filter (typstGeometryFromPaperWidth)
// IMPORTANT: This must come AFTER book.with() to override the book format's margin settings
#import "@preview/marginalia:0.3.1" as marginalia
#show: marginalia.setup.with(
inner: (
far: $margin-geometry.inner.far$,
width: $margin-geometry.inner.width$,
sep: $margin-geometry.inner.separation$,
),
outer: (
far: $margin-geometry.outer.far$,
width: $margin-geometry.outer.width$,
sep: $margin-geometry.outer.separation$,
),
top: $if(margin.top)$$margin.top$$else$1.25in$endif$,
bottom: $if(margin.bottom)$$margin.bottom$$else$1.25in$endif$,
// CRITICAL: Enable book mode for recto/verso awareness
book: true,
clearance: $margin-geometry.clearance$,
)
$endif$