aboutsummaryrefslogtreecommitdiff
path: root/lib/render/view.ml
Commit message (Collapse)AuthorAgeFilesLines
* fix(render): derive citation keys from the day, not a hardcoded pair (W5)Lukasz Kasprzak2026-08-271-7/+81
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | lib/render/view.ml hardcoded exactly two template keys, "first" and "gospel", regardless of what a day's citations actually contain. The OF now resolves a third reading, Second, on Sundays and solemnities (OLM 1981 Praenotanda n. 69.1/n. 84(b)(c)) via the rite-supplied Rite.t.citation_shapes, which the render layer could not express. Latent today (bin/main.ml's reject_rite_for refuses --rite on every command that reaches View.of_days, so only EF ever reaches this path), but the wrong shape to carry forward. View.citation_fields now derives one key per Citation.part ACTUALLY PRESENT in a day's own citations, in Citation.all_parts order, using Citation.part_to_string for the key name. An absent part contributes no key at all, not a key holding the empty string: Template.render already treats a missing key and a present Str "" identically in Var, Section and Inverted position, so nothing is lost, and Emit_json.year dumps the whole Obj verbatim, so an unconditionally-present-but-empty key for a still-unbuilt part (Psalm/Tract/Alleluia/Sequence) would sit in every EF day's JSON forever. Filtering to present parts alone is what makes EF's output identical byte for byte BY CONSTRUCTION rather than by a rite check: EF's citations are always exactly [First; Gospel], so the derived key set is always {"first"; "gospel"}, in that order. padding_cell keeps its own fixed "first"/"gospel" pair rather than deriving from a (nonexistent) day: it mirrors the closed set every rite currently reachable through this render path (EF alone) always carries, documented as debt for whenever a rite whose citation_shapes includes Second is admitted here -- test_padding_and_real_share_key_set would catch the mismatch immediately. Downstream consumers: Emit_json needed no change (already a verbatim dump). Emit_xml and Emit_ics gained a third, order-preserving element/line (a <citation part="second"> element; a "Second ..." DESCRIPTION line spliced between Epistle and Gospel) since neither is a fixed-column format. Emit_csv is deliberately left unwidened and commented: RFC 4180's header-row contract makes a variable column count illegal, and OF cannot reach `emit` today regardless, so there is nothing to widen for yet; admitting OF here later needs a disclosed, deliberate format change, not a silent one. term_keys (line ~239) is left unchanged, with a comment recording the judgment call: "epistle" and "lesson" are indeed EF/Roman vocabulary (Epistola vs. a non-apostolic Lectio, e.g. an Ember day's Old Testament reading) sitting in an otherwise rite-agnostic list, but nothing forces a fix yet -- no OF template exists, and every in-repo EF template already reads term.epistle, so renaming it would break 11 shipped templates and any user template for zero present benefit. "lesson" already sits unused, already translated in both lang files, and is the natural neutral term a future OF ordo template would reach for. Tests: test_view.ml gains test_ef_days_never_carry_a_second_key (a whole year, not one day) and test_three_citation_day_exposes_all_three, driving View.of_days directly with a hand-built Liturgical_day.t since OF cannot reach this path through the CLI yet -- the only honest way to exercise a latent path. test_emit.ml/test_ics.ml extend coverage to Emit_xml/Emit_ics (presence, value, and reading order) and pin Emit_csv's deliberate 16-column non-widening. EF byte-identity: zero diff in data/ef/ or lib/rites/rite_ef/ against the pre-change commit. `colitur day`, `readings`, `emit --format csv/xml/json/ ics`, and `table --template templates/ef/ordo.txt` for 2026, all non-empty, all byte-identical (cmp) against a worktree build of the pre-change commit.
* fix(render): a month answers to both spellings of its own nameLukasz Kasprzak2026-08-201-1/+13
| | | | | | | | | | | | | | | | A week object carries month_name/month_num/month_abbr, because at that level a bare `name` would be ambiguous. An author who learned those names inside {{#weeks}} reaches for them one level up inside {{#months}} too -- where they resolved to nothing, and an unknown key renders as the empty string by design, so the result was a silently blank month heading rather than any error. Found by writing a template from scratch rather than copying a shipped one; every shipped template sidesteps it, which is why nothing caught it. The aliases are additive, so no existing template or golden changes. colitur-templates(5) now documents both spellings and says why they exist.
* feat(render): citations render through Sigla at both call sitesLukasz Kasprzak2026-08-201-7/+11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | View.citation_ref and readings' part_ref are the only two places a citation reaches output; every emitter goes through View. --raw passes Sigla.verbatim rather than a style built over Lang.raw: an identity name table would still reformat punctuation and renumber. Loosen reject_sigla_for: --sigla-style/--sigla-book/--sigla-tradition now actually render on readings/table/render/emit/publish (each builds its own Sigla.t via the new load_sigla), and stay refused only on commands that render no citation (day, easter, temporal, ...). names_of degrades a Lang.bible miss to Book.default_spelling rather than printing the lookup key itself ("luke.abbr"); pinned in cli.t against a language file with no [bible] section at all, independent of la.ini/en.ini's own eventual [bible] section. Regenerate the 11 golden templates for 2027: every changed line is one canonical citation replacing a stored variant, verified against the full readings 2027 diff (raw vs default) cell by cell -- 86 distinct (raw, rendered) pairs account for the entire diff across all 11 files, with zero unexplained residue. Two families: the seven duplicate book spellings collapsing onto one canonical form (Isa./Isa, 3 Kgs./3 Kings, ...), and citations reconstructed from parsed structure dropping stray punctuation the parser already treats as noise (a trailing period or semicolon, a comma chapter/verse separator, an elided inherited chapter) -- each of the latter already named and tested in test_citation.ml's parse_suite before this task.
* feat(render): Roman week numbers and date spans, as dataLukasz Kasprzak2026-08-201-7/+77
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The ordo booklet's week header repeated the month name on every week even though the heading above already established it -- pure noise. Replace it with a Roman week number plus the span of dates the week covers, e.g. "Hebdomada I (Ian 1-2)", following the project's own rule that a presentation choice is data, not code. lang/{la,en}.ini gain a [month_abbr] section (three-letter month abbreviations); Lang.month_abbr follows Lang.month's exact shape, including the out-of-range and miss-returns-the-key contracts. The coverage test now fails loudly if an abbreviation goes missing, the same as [month] already does. Every week object in the view gains num_roman (Roman numeral, num stays as the arabic original -- Roman is a presentation choice, not an engine change), first_dom/last_dom (the day-of-month of the week's first and last IN-MONTH days, padding excluded), month_abbr (resolved through Lang.month_abbr), and single_day (true when the week holds exactly one in-month day). single_day is a flag, not a preformatted span string: the engine is logic-less and cannot itself decide between "Ian 1" and "Ian 1-2", so a template makes that call from the flag instead -- the same "shape the data, not the template" discipline in_month and last already follow. Weeks are built per month with padding only at the two ends, so a week's in-month days never cross a month boundary -- verified, not assumed: every week always has at least one real day since no month is shorter than a single week. Covered by three new View tests, including a real single-day-week witness (January 2027's own trailing week is a lone Sunday, the 31st).
* feat(templates): the ordo booklet flows continuously, not one week per pageLukasz Kasprzak2026-08-201-0/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | One week per page filled roughly the top third of every A5 page and cost 65 pages for a year that needed far fewer -- the original requirement, reversed here: a full 7-day week already occupies most of an A5 page even at reduced type, so the real saving was always in the year's many SHORT boundary weeks (a month's first/last week is rarely a full seven days), not in packing two full weeks together. lib/render/view.ml gains a [first] field on every week object (true on a month's own first week, the same 'cheap flag beats invented template logic' call [last] already made for grid rows) -- there is still no {{../}} parent-path syntax for a template to test this any other way. ordo.tex: the per-week \clearpage is gone. Every week keeps its existing small header line; the month's own first week additionally prints a large, ruled banner. \needspace (a template-local LaTeX dependency, not one of the frozen OCaml deps) keeps a header from being stranded alone at a page's bottom -- and a real, separate bug surfaced building it: \label placed BEFORE \needspace captured the OLD page number when \needspace went on to force a break, producing a step-stale \pageref in the TOC (fixed by moving \label after both needspace checks). The day box itself is tightened (rank/colour folded onto the same line as the citations -- an A5 box is far wider than tall, so this fits even Holy Week's longest citations with room to spare) so a second week routinely shares a page with the first. Measured result: 65 -> 53 pages, two pdflatex passes still settle the TOC, zero orphaned headers, zero overfull boxes. ordo.typ: the per-week #pagebreak is gone; #show heading's own block gains sticky:true, Typst's built-in answer to what ordo.tex needs \needspace for. The month banner needed its OWN sticky:true, found live: without it a banner could sit alone at a page's bottom with the entire month pushed to the next page -- not caught by a check for an orphaned HEADING, since the banner is plain markup, not one. Measured result: 65 -> 27 pages (Typst's own page-fill packs tighter than ordo.tex's needspace-driven approach), one compile pass, zero orphans. The five non-paginated flavours (ms/html/md/adoc/txt) never had a forced page break to remove -- they mainly needed the same header treatment for structural consistency: a week-level header nested under each month's existing one, walking months>weeks>days (the week's own padded, in-month-guarded day list) in place of the old months>days flat loop. Verified against groff, asciidoctor and an HTML parser -- zero warnings. Goldens regenerated through the Test_view.view_of path test_render_golden.ml itself uses, not the CLI (whose --lang default would have pinned output the tests never produce).
* fix(naming): the rite prints its display name, not the internal id efLukasz Kasprzak2026-08-201-0/+6
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The booklet's title page and running header read "Ordo 2027 . ef" -- an internal identifier reaching a reader, the same defect class the sanctoral/temporal slugs were already fixed for. lang/la.ini and lang/en.ini each gain a [rite] section mapping the rite id to a reader-facing name, sourced from the Missal's own title page (docs/research/LT.txt:6 "MISSALE ROMANUM", :15 "EDITIO TYPICA") plus the 1962 dating this project uses throughout (rules-register.md, CLAUDE.md) -- both cited in the ini comments, marked PATTERN since the phrase combines two title-page elements rather than quoting one verbatim heading. Lang.t gains a rite table/lookup (same total, miss-echoes-the-key contract as every other lookup here); View.of_days exposes rite_name alongside the existing rite field, which stays the stable key exactly as slug is kept beside name. Every shipped template that printed {{rite}} now prints {{rite_name}} instead. test_lang_coverage.ml gains a coverage assertion over every rite id the engine can emit -- one today -- so a second rite module (OF) landing without a matching [rite] entry fails loudly instead of printing its own bare id. Goldens regenerated through the Test_view.view_of path test_render_golden.ml itself uses (English with a Latin fallback), not the CLI (whose --lang default is plain Latin and would have pinned output the tests never produce). test/cli.t's own JSON prefix assertion updated to match: rite_name is a real new key in that generic view dump, sitting right after rite.
* feat(render): names reach the view and every emitterLukasz Kasprzak2026-08-191-36/+85
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The view's name is now the RESOLVED display string and slug is untouched, so machine formats carry both -- a script keeps the stable key, a human reads the name. name is a plain string, not a lang-keyed object. That removes the shadowing hazard outright: a dotted {{name.la}} used to fall back WHOLESALE to the enclosing month's name.la and print Ianuarius on every unnamed day, which is how a printed booklet came to show the month where the feast belonged. weekday, season, rank and colour all gain localised companions, because a calendar in a language needs more than feast names, and templates gain a term vocabulary so fixed strings need no template edit to translate. Asserted over a whole year: no day renders its slug as its name. Beyond the brief's own code sample: - comm_value's own name is now resolved through the same lang.celebration table too (not only the observed day's), because Task 8's own ordo template interpolates a plain {{name}} inside {{#comms}} -- an Obj there would render silently blank. A commemoration slug without Latin coverage still degrades to the slug, same as everywhere else in this system; that is a lang/la.ini DATA gap (113 of 327 sanctoral slugs, measured), not a regression this task introduced. - bin/main.ml's emit/table/publish call sites needed ~lang to compile at all, which is collateral from the of_days signature change, not this task's own file list. Rather than pass Lang.raw and ship the very slug-as-name defect this branch exists to fix, they load the shipped Latin table by the same probe order data_dir() already uses -- a deliberate, commented BRIDGE that Task 6 replaces wholesale with real --lang/--raw/config resolution. bin/dune gained colitur_naming accordingly. - test/cli.t needed two related fixes to stay green: the CSV header/row example, and a table/LaTeX escaping demonstration that relied on the kernel's own English name for Sts Peter & Paul -- gone from the view now that name resolves through lang tables only, and the Missal's own Latin spells the feast with et, never an ampersand. Escaping itself is still proved live on 2035 data in test_emit.ml. - Both schemas gained the new day/week/top-level keys (season_name, weekday, rank_name, colour_name, term, weekday_headings, month_num, month_name), not only the name shape change; schema/colitur-v1.xsd verified against real emitted XML via xmllint (make check-schema). Render/golden's 9 cases (the shipped ordo/grid templates, all six flavours) now fail as expected: their old {{name.la}} / {{#name}}... idiom finds nothing on a plain string. That is Tasks 8/9's own scope to rewrite, per the plan's own pre-flight conflict scan -- not fixed here, and not silently pinned by regenerating goldens off broken output. 495 tests run (490 + 5 new), 486 pass; the 9 failures are exactly Render/golden's ordo/grid cases.
* fix(render): remove rank_label -- it duplicated name verbatimLukasz Kasprzak2026-08-191-2/+6
| | | | | | | | | | | | | | | | | | view.ml's rank_label field was a byte-for-byte copy of the celebration's name (names_value cel.Celebration.names), not a localized rank label at all -- the kernel has no per-language rank names to draw one from, so there was no honest value to put there. Nothing consumed it: no template in the plan, no test, no other code referenced it. Removed from both day_value and padding_cell so the two key sets stay identical (23 keys each, verified). schema/day-v1.json already described 23 keys and needed no change -- it now matches the emitted output exactly. schema/day-v1.json is a published contract: once a phone subscribes or a site fetches this, removing a field is a breaking /v2/ change. The time to remove a field that lies about its own contents is before anyone can depend on it, not after.
* feat(render): the view modelLukasz Kasprzak2026-08-191-0/+145
Shapes a civil year of resolved days into the value a template renders against. This layer is why the engine can stay logic-less: a month grid needs leading blank cells, week bucketing and an in-month test, and a logic-less template can compute none of it. Both weeks and days are offered at every level -- the booklet walks days, the grid walks weeks -- so the two artefacts cannot drift. Colours are six booleans, not hex: hex bakes a presentation policy into the engine, and LaTeX, groff and HTML each want a different colour expression. Asserted: exactly one of the six is true on every day of a whole year, so a template keying off them can never get none or two. Padding cells carry every field a real day carries, empty, so a template never hits a missing key mid-grid.