aboutsummaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 10:07:15 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 10:07:37 +0200
commit6aebb3ec513c0177a39c2eb8d4b741e90d6f53f4 (patch)
tree3fb327d0b3fa9ffd67d493c7393cdf9a963bf8b9 /templates
parent66543815ae6cbf6e576a889c9291678744243953 (diff)
downloadcolitur-6aebb3ec513c0177a39c2eb8d4b741e90d6f53f4.tar.gz
colitur-6aebb3ec513c0177a39c2eb8d4b741e90d6f53f4.zip
feat(templates): ordo booklet and wall calendar in Typst
Adds templates/ef/ordo.typ (A5, one week per page, each day framed with a colour swatch, real names, every fixed string through term.*) and templates/ef/grid.typ (A4 landscape, one month per page, 7 columns, colour-filled cells, Epistle/Gospel sigla), matching what ordo.tex and grid.tex already do. Typst resolves its own table of contents inside a single compile: no pageref/aux-file dance, unlike pdflatex's own two-pass need for ordo.tex. ordo.typ's TOC is one flat heading per week (Month . Week N) rather than ordo.tex's own month-once/weeks-nested layout, since the view model has no first-of-month flag for a template to test and inventing one would be new engine state for a cosmetic grouping. grid.typ deliberately diverges from grid.tex's own partial-fill behaviour for a 5-week month: Typst's row-size list repeats its own last entry for every remaining row, so a two-entry rows spec fills the page completely regardless of week count, where LaTeX's fixed cellh leaves a 5-week month's bottom sixth blank on purpose. Both divergences are documented in the templates' own headers. Two bugs found and fixed while building these against the real typst 0.14.2 binary: two stray hash characters in grid.typ's header comment that were not actually inside a line-comment (would have been parsed as Typst code, not prose); and two lines merging into one wherever they were separated only by a bare newline (still the same soft-wrapped paragraph in Typst, unlike LaTeX's own explicit paragraph break) -- fixed with a trailing backslash to force a real line break without adding inter-paragraph spacing, verified against a full-year render (zero typst warnings across all 365 days of 2027, in both templates). Goldens (test/golden/ordo-2027.typ, test/golden/grid-2027.typ) were generated through the harness's own View.of_days/Template.render_string path, via a throwaway generator, NOT through the CLI: the CLI's default language table differs from the harness's own English-chained-to-Latin table, confirmed live (diffing CLI output against the harness's own render showed exactly the expected language divergence and nothing else) -- the same trap this project has hit before. make check-templates now typesets both templates with the typst binary, using the same SKIPPED-and-exit-0 shape the pdflatex/groff blocks already use when their own tool is absent, verified in both directions: absent (PATH without typst) skips loudly and exits 0, and a deliberately corrupted template fails the target with a non-zero exit and typst's own error text.
Diffstat (limited to 'templates')
-rw-r--r--templates/ef/grid.typ105
-rw-r--r--templates/ef/ordo.typ121
2 files changed, 226 insertions, 0 deletions
diff --git a/templates/ef/grid.typ b/templates/ef/grid.typ
new file mode 100644
index 0000000..c197ad2
--- /dev/null
+++ b/templates/ef/grid.typ
@@ -0,0 +1,105 @@
+// colitur wall calendar -- A4 landscape, one month per page. Typst. flavour: typst
+// Build: colitur table --year 2027 --template grid.typ > grid.typ && typst compile grid.typ
+// ONE compile pass: this template has no table of contents and no
+// cross-references (unlike ordo.typ), so there is nothing for a second pass
+// to resolve even in principle -- unlike pdflatex's own two-pass need for
+// grid.tex's sibling ordo.tex, though grid.tex itself already needed only
+// one pass too.
+//
+// The grid must FILL the page, not sit in its top quarter. grid.tex's own
+// header explains why it computes an explicit fixed row height by hand
+// (\cellh) and why that leaves a 5-week month only 5/6 full rather than
+// 6/6 -- LaTeX's tabular has no notion of "share out whatever is left".
+// Typst's table/grid DOES: a row-size list shorter than the actual row
+// count repeats its OWN LAST entry for every remaining row (verified live:
+// a 5-row table given `rows: (auto, 1fr)` renders five equal 1fr rows, not
+// one), so `rows: (auto, 1fr)` below fills the page COMPLETELY regardless
+// of whether the month has 5 or 6 Sunday-started weeks, with no week count
+// arithmetic in this file at all. This is a genuine, disclosed DIVERGENCE
+// from grid.tex's own behaviour, not an oversight: LaTeX leaves a 5-week
+// month's bottom sixth blank on purpose (documented there); this template
+// instead stretches every month to fill the whole page every time, because
+// Typst makes that the simpler option, not the harder one.
+//
+// Real names, not slugs: the observed day's own display name is a PLAIN
+// resolved string (View.of_days), interpolated directly as a plain var
+// below -- there is no dotted-la-with-slug-fallback idiom left to write
+// (see ordo.typ's own header for the full reasoning; it applies here
+// unchanged).
+//
+// The weekday header row comes from the view's top-level weekday_headings
+// list (name/last objects), not a hard-coded Dom/Lun/Mar row: the engine
+// rejects an EMPTY tag path -- a bare dot inside a section, on its own --
+// as a parse error, so the loop below names a field on each heading object
+// rather than interpolating the section's own value directly, and a
+// hard-coded row would not be localised anyway.
+//
+// No separator flag needed: grid.tex's own `last` field exists because a
+// LaTeX tabular row needs its ampersand BETWEEN cells, never after the
+// final one, and the engine has no "unless last" construct to lean on
+// instead -- so that template renders the flag as data. Typst's own
+// #table() takes a flat, comma-separated argument list and wraps it into
+// rows itself, by column count, so the SAME rule applies to every cell,
+// header row included, with no special case for the last one -- verified
+// live that a trailing comma before a function call's closing parenthesis
+// is accepted, not a syntax error. `last` is therefore simply unused here;
+// left in the view model for grid.tex's own sake, not removed for this one.
+//
+// Sigla, compactly: each cell also carries the day's Epistle and Gospel
+// references (view fields first/gospel), joined by a centred dot rather
+// than repeating Ep./Ev. labels -- the same choice grid.tex and grid.ms
+// already made, kept consistent rather than invented a second way. A
+// #v(1fr) inside each cell's own content -- verified live to work the same
+// way inside a table cell as inside any other block -- pushes the rank
+// line and the sigla to the bottom of the cell, leaving the day number and
+// name at the top, so the two short lines never collide with a long entry
+// above them the way a single crammed line would.
+#set page(paper: "a4", flipped: true, margin: 10mm, numbering: none)
+#set text(size: 8pt)
+#set par(justify: false)
+
+#let cgrid = (
+ white: rgb("#FFFFFF"), red: rgb("#F8DCDC"), green: rgb("#DDEEDD"),
+ violet: rgb("#E6DAF0"), rose: rgb("#FADCE8"), black: rgb("#DCDCDC"),
+)
+#let cpad = luma(248)
+
+{{#months}}
+#text(size: 20pt, weight: "bold")[{{name}} {{year}}]
+#v(2mm)
+#table(
+ columns: (1fr,) * 7,
+ rows: (auto, 1fr),
+ stroke: 0.4pt + luma(150),
+ inset: 1.6mm,
+ align: left + top,
+ {{#weekday_headings}}
+ table.cell(align: center + horizon)[#text(weight: "bold")[{{name}}]],
+ {{/weekday_headings}}
+ {{#weeks}}
+ {{#days}}
+ {{#in_month}}
+ table.cell(fill: cgrid.at("{{colour}}"))[
+ // One paragraph, a trailing backslash between the rank line and the
+ // sigla line rather than a bare newline: two markup lines joined only
+ // by a bare newline stay on ONE rendered line (still the same soft-
+ // wrapped paragraph) -- verified live, the first draft of this cell
+ // merged "III classis" straight into the citation. grid.tex's own
+ // header explains why the rank line and the sigla get their own line
+ // EACH rather than being crammed onto one -- the same reasoning
+ // applies here, so this keeps the two-line shape it settled on.
+ #set par(leading: 0.35em)
+ #text(weight: "bold")[{{dom}}] #text(size: 7pt)[{{name}}]
+ #v(1fr)
+ #text(size: 6.5pt)[{{rank_name}}] \
+ #text(size: 6.5pt)[{{#first}}{{first}}{{/first}}{{#gospel}}{{#first}} #sym.dot.c {{/first}}{{gospel}}{{/gospel}}]
+ ],
+ {{/in_month}}
+ {{^in_month}}
+ table.cell(fill: cpad)[],
+ {{/in_month}}
+ {{/days}}
+ {{/weeks}}
+)
+#pagebreak(weak: true)
+{{/months}}
diff --git a/templates/ef/ordo.typ b/templates/ef/ordo.typ
new file mode 100644
index 0000000..33bc802
--- /dev/null
+++ b/templates/ef/ordo.typ
@@ -0,0 +1,121 @@
+// colitur ordo booklet -- Typst. flavour: typst
+// Build: colitur table --year 2027 --template ordo.typ > ordo.typ && typst compile ordo.typ
+//
+// ONE compile pass, not two: Typst resolves its own table of contents (built
+// from headings via the outline function below) and every page number it
+// shows internally, inside a single invocation -- there is no LaTeX-style
+// pageref/aux-file dance and no "run it twice so the numbers settle". That is
+// a genuine advantage of this flavour over ordo.tex, not merely a different
+// syntax for the same thing.
+//
+// A5, one week per page, each day in a framed box with a colour swatch.
+// Every fixed string (headings, the Epistle/Gospel/Commemoration/Week
+// labels) comes from the view's term vocabulary rather than being written
+// into this file, so a translated booklet needs no template edit -- only a
+// different --lang. Same discipline as ordo.tex; see that file's own header
+// for the full reasoning, repeated only where Typst changes it below.
+//
+// The observed day's own display name is a PLAIN resolved string
+// (View.of_days), never a lang-keyed object, exactly as in every other
+// shipped flavour -- there is no dotted-la-with-slug-fallback idiom to
+// write here at all.
+//
+// The engine has no parent-path syntax: nested inside a month's own week
+// loop, a bare week-number reference finds the WEEK's own number, and there
+// is no way to reach the enclosing month's from there. That is why each
+// week object carries its own month number and month name fields
+// (lib/render/view.ml), used below via month_name/month_num instead of a
+// parent-path reference the engine cannot express.
+//
+// The engine also rejects an empty tag path outright (a bare-dot section or
+// variable is a parse error, not an invented "current context" behaviour),
+// so every loop below names a field on the object it walks rather than
+// trying to interpolate the object itself.
+//
+// This template's own line comments are plain text to the engine: it has no
+// awareness of Typst's // syntax, and a stray double-brace pair inside one
+// would still be parsed as a tag -- which is why this whole header, like
+// ordo.tex's own, is written without ever typing two curly braces next to
+// each other, even to name a field.
+//
+// One TOC level, not two: LaTeX's own ordo.tex prints the month name ONCE
+// per month with its weeks dotted-and-paginated underneath, because it
+// walks the months-then-weeks nesting directly in its own hand-built TOC
+// loop. Typst's outline instead builds its table of contents automatically
+// from real document headings, and there is no "is this the month's first
+// week" flag in the view model for a template to test -- inventing one
+// would be new engine state for a cosmetic grouping, not a real gap. Each
+// week page therefore carries ONE flat heading, "Month . Week N", which is
+// both what a reader sees at the top of that page and, automatically, its
+// own single-line entry in the table of contents below.
+//
+// Colour swatch: the six colour names this rite emits (white, red, green,
+// violet, rose, black -- Colour.to_string's own closed vocabulary) are used
+// as literal dictionary keys inside a Typst string argument below, the same
+// trick ordo.tex's own \swatch{licol...} macro-name concatenation relies
+// on. Both templates depend on that vocabulary staying plain lowercase
+// ASCII words with no markup metacharacter in it; the escaper still runs
+// over the substituted value regardless; a value outside that closed set
+// would need a different placement, not a different escaper.
+#set page(
+ paper: "a5",
+ margin: (inside: 14mm, outside: 10mm, top: 12mm, bottom: 12mm),
+ numbering: "1",
+ header: align(center)[#text(size: 8pt)[{{term.ordo}} {{year}} #sym.dot.c {{rite}}]],
+)
+#set text(size: 9pt)
+#set par(justify: false)
+#show heading: it => {
+ set text(size: 12pt, weight: "bold")
+ block(above: 3mm, below: 2mm)[#it.body]
+}
+
+#let licol = (
+ white: rgb("#FFFFFF"), red: rgb("#C1272D"), green: rgb("#2E7D32"),
+ violet: rgb("#6A1B9A"), rose: rgb("#E91E8C"), black: rgb("#000000"),
+)
+
+// A framed swatch, always stroked in black regardless of fill -- so white
+// (and, were the page ever black, black) still reads as a mark rather than
+// a gap in the row. Mirrors ordo.tex's own \swatch macro.
+#let swatch(colour) = box(width: 2.4mm, height: 2.4mm, fill: licol.at(colour), stroke: 0.4pt + black)
+
+#align(center)[
+ #text(size: 18pt, weight: "bold")[{{term.ordo}} {{year}}] \
+ {{rite}}
+]
+#v(4mm)
+#text(weight: "bold")[{{term.contents}}]
+#v(1mm)
+#outline(title: none, depth: 1, indent: 4mm)
+#pagebreak(weak: true)
+
+{{#months}}
+{{#weeks}}
+= {{month_name}} #sym.dot.c {{term.week}} {{num}}
+
+{{#days}}
+{{#in_month}}
+#block(width: 100%, above: 0.6mm, below: 0mm, breakable: false, stroke: 0.4pt + luma(160), inset: (x: 1.6mm, y: 0.6mm))[
+ // A single Typst paragraph throughout, deliberately: two markup lines
+ // separated only by a bare newline stay on ONE rendered line (they are
+ // still the same paragraph, soft-wrapped) -- verified live, the first
+ // draft of this box merged the rank/colour line into the citation line
+ // for exactly that reason. A trailing backslash forces a real line
+ // break WITHOUT starting a new paragraph (no inter-paragraph spacing
+ // to tune down), which is what a compact multi-line box wants; #par's
+ // own block-level break, used further up for the day/weekday/swatch
+ // row against the name below it, is reserved for the one place this
+ // box actually wants a fresh paragraph.
+ #set par(leading: 0.42em)
+ #text(weight: "bold")[{{dom}}] #h(2mm) #text(size: 8pt)[{{weekday}}] #h(1fr) #swatch("{{colour}}") \
+ #text(weight: "bold")[{{name}}] \
+ #text(size: 7pt)[{{rank_name}} #sym.dot.c {{colour_name}}] \
+ #text(size: 7pt)[{{#first}}{{term.epistle}} {{first}}{{/first}}{{#gospel}}{{#first}} #h(2mm) {{/first}}{{term.gospel}} {{gospel}}{{/gospel}}]{{#comms}} \
+ #text(size: 7pt)[{{term.commemoration}} {{name}}]{{/comms}}
+]
+{{/in_month}}
+{{/days}}
+#pagebreak(weak: true)
+{{/weeks}}
+{{/months}}