diff --git a/_extensions/MHellmund/julia-color/LICENSE_quarto_bookext.txt b/_extensions/MHellmund/julia-color/LICENSE_quarto_bookext.txt new file mode 100644 index 0000000..a9be13f --- /dev/null +++ b/_extensions/MHellmund/julia-color/LICENSE_quarto_bookext.txt @@ -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. diff --git a/_extensions/MHellmund/julia-color/_extension.yml b/_extensions/MHellmund/julia-color/_extension.yml index ac280a4..a1da862 100644 --- a/_extensions/MHellmund/julia-color/_extension.yml +++ b/_extensions/MHellmund/julia-color/_extension.yml @@ -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 diff --git a/_extensions/MHellmund/julia-color/ansi2htmltex.lua b/_extensions/MHellmund/julia-color/ansi2htmltex.lua index c8050c5..12019b9 100644 --- a/_extensions/MHellmund/julia-color/ansi2htmltex.lua +++ b/_extensions/MHellmund/julia-color/ansi2htmltex.lua @@ -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..">","" 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}, } diff --git a/_extensions/MHellmund/julia-color/numbering.typ b/_extensions/MHellmund/julia-color/numbering.typ new file mode 100644 index 0000000..2f0e3ce --- /dev/null +++ b/_extensions/MHellmund/julia-color/numbering.typ @@ -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 + ] +} diff --git a/_extensions/MHellmund/julia-color/orange-book.lua b/_extensions/MHellmund/julia-color/orange-book.lua new file mode 100644 index 0000000..4729030 --- /dev/null +++ b/_extensions/MHellmund/julia-color/orange-book.lua @@ -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 +}) diff --git a/_extensions/MHellmund/julia-color/page.typ b/_extensions/MHellmund/julia-color/page.typ new file mode 100644 index 0000000..488bf47 --- /dev/null +++ b/_extensions/MHellmund/julia-color/page.typ @@ -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 diff --git a/_extensions/MHellmund/julia-color/resources/typst/juliainc.typ b/_extensions/MHellmund/julia-color/resources/typst/juliainc.typ index ba12ab3..580a58c 100644 --- a/_extensions/MHellmund/julia-color/resources/typst/juliainc.typ +++ b/_extensions/MHellmund/julia-color/resources/typst/juliainc.typ @@ -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) diff --git a/_extensions/MHellmund/julia-color/typst-show.typ b/_extensions/MHellmund/julia-color/typst-show.typ new file mode 100644 index 0000000..337128f --- /dev/null +++ b/_extensions/MHellmund/julia-color/typst-show.typ @@ -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$