// 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 -- AND every // month must fit on exactly ONE page, never spill onto a second (Defect 3; // this file used to read `rows: (auto, 1fr)`, described below as filling // the page "completely regardless of 5 or 6 rows"). That description was // true for row COUNT but silently assumed a fixed CONTENT height per row // too: adding the sigla line grew each cell's natural content past what a // 1/6-of-remaining-space `1fr` row allows, and a Typst `fr` track's size is // the LARGER of its fair share and its content's own minimum -- it grows // PAST 1fr rather than shrinking content to fit, which is exactly how a // 6-row month (`I classis` sliced mid-cell, the next month's own heading // printed underneath it) spilled onto a second page. Fixed now the same // way grid.tex's own \cellh already was: an EXPLICIT row height // ([cellh] below, computed from the page's own text height divided by six // -- every month in the 1583-9999 domain has 5 or 6 Sunday-started weeks, // never more), with each cell's own content wrapped in a height-clipped // block sized to match. A 5-week month still leaves a blank sixth row (the // same trade-off grid.tex's own header already discloses and defends); // the disclosed DIVERGENCE this comment used to describe -- Typst // stretching every month to fill the page regardless of row count -- is // GONE: overflow safety now matters more than that cosmetic fill, and a // fixed height is what makes the fit provable rather than hoped for. // // 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) // a4 landscape text area is 277x190mm at this page's own 10mm margin. // [titleh] is the measured height of the month title line + its own // #v below (16pt bold, one line, verified against real rendered output, // not assumed); [headerh] the weekday-name row. What is left // (190 - titleh - headerh) is divided by SIX, never five, so a 6-row // month is what this budget is proven against -- a 5-row month then has // one row's worth of genuine spare room, not a tight fit. [cellinset] is // subtracted back out per side when sizing each cell's own clipped inner // block below. // Budgeted against 184mm, not the full 190mm text height: a first attempt // at exactly 190mm (zero slack) measurably split the table BETWEEN rows // on real output -- Typst's own table breaks across a page boundary by // default when the sum of explicit row heights doesn't quite clear the // remaining space, stranding the 6th row alone on its own page with no // heading -- so 6mm of genuine slack is deliberate, not rounded for // looks. `breakable: false` below is the second, independent guard: even // if this budget is ever wrong again, the WHOLE table jumps to a fresh // page rather than splitting a row across two -- loudly wrong (an odd // blank page) rather than silently wrong (a sliced row). It is a #block // property, not a #table one (Typst rejects `table(breakable: ...)` // outright as an unexpected argument -- verified live), so the table // below is wrapped in a block rather than passed the flag directly. // // Each cell's own content sits in a SECOND, height-clipped block, exactly // [cellh] minus its own top/bottom inset: the table row height is // EXPLICIT ([cellh], not `auto` or `1fr`), so nothing inside can grow the // row past it -- content that still does not fit is clipped rather than // spilling into the next cell or the next page, the "truncate // gracefully" option Defect 3's own brief names. Never actually hit on // real 1583-9999 data (checked against the full 2027 fixture, every // month, both row counts -- see the task report), but the box is // provably safe either way, which a content-driven row never was. Inside // that block, one Typst paragraph throughout, 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. #let titleh = 9mm #let headerh = 6mm #let cellh = (184mm - titleh - headerh) / 6 #let cellinset = 1.2mm {{#months}} #text(size: 16pt, weight: "bold")[{{name}} {{year}}] #v(1.5mm) #block(breakable: false)[ #table( columns: (1fr,) * 7, rows: (headerh, cellh), stroke: 0.4pt + luma(150), inset: cellinset, align: left + top, {{#weekday_headings}} table.cell(align: center + horizon)[#text(size: 7pt, weight: "bold")[{{name}}]], {{/weekday_headings}} {{#weeks}} {{#days}} {{#in_month}} table.cell(fill: cgrid.at("{{colour}}"))[ #block(height: cellh - 2 * cellinset, clip: true)[ #set par(leading: 0.3em) #text(weight: "bold")[{{dom}}] #text(size: 6.5pt)[{{name}}] #v(1fr) #text(size: 6pt)[{{rank_name}}] \ #text(size: 6pt)[{{#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}}