diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 15:29:33 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 15:29:33 +0200 |
| commit | 6d367ab8e90f6e713d262a7f19fb908b28d4796a (patch) | |
| tree | f5377f426ebe5ba32b7a8532850eafc9c103b9f9 /lib/render | |
| parent | 92559825f9ad3e0f751d2c2022ffe000f23358ef (diff) | |
| download | colitur-6d367ab8e90f6e713d262a7f19fb908b28d4796a.tar.gz colitur-6d367ab8e90f6e713d262a7f19fb908b28d4796a.zip | |
feat(render): names reach the view and every emitter
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.
Diffstat (limited to 'lib/render')
| -rw-r--r-- | lib/render/dune | 2 | ||||
| -rw-r--r-- | lib/render/emit_csv.ml | 16 | ||||
| -rw-r--r-- | lib/render/emit_ics.ml | 12 | ||||
| -rw-r--r-- | lib/render/emit_xml.ml | 17 | ||||
| -rw-r--r-- | lib/render/view.ml | 121 | ||||
| -rw-r--r-- | lib/render/view.mli | 18 |
6 files changed, 124 insertions, 62 deletions
diff --git a/lib/render/dune b/lib/render/dune index 7880188..548cf43 100644 --- a/lib/render/dune +++ b/lib/render/dune @@ -1,5 +1,5 @@ (library (name colitur_render) - (libraries colitur_kernel sexplib) + (libraries colitur_kernel colitur_naming sexplib) (preprocess (pps ppx_sexp_conv))) diff --git a/lib/render/emit_csv.ml b/lib/render/emit_csv.ml index b478265..415a684 100644 --- a/lib/render/emit_csv.ml +++ b/lib/render/emit_csv.ml @@ -16,11 +16,15 @@ let escape_field s = let get v k = match v with T.Obj kvs -> List.assoc_opt k kvs | _ -> None let s v k = match get v k with Some (T.Str x) -> x | _ -> "" -let nested v a b = match get v a with Some inner -> s inner b | None -> "" +(* [name] is now the single RESOLVED display string (view.ml), not a + lang-keyed object -- the [name_la]/[name_en] pair this replaces carried + the kernel's own [Celebration.names], a different, unlocalised source. + Machine formats still carry [slug] alongside it, so a script keeps the + stable key and a human reads the name. *) let columns = - [ "date"; "rite"; "season"; "week"; "slug"; "rank"; "colour"; "subject"; - "name_la"; "name_en"; "first"; "gospel"; "comms" ] + [ "date"; "rite"; "season"; "season_name"; "week"; "slug"; "name"; "weekday"; + "rank"; "rank_name"; "colour"; "colour_name"; "subject"; "first"; "gospel"; "comms" ] let row ~rite d = let comms = @@ -30,9 +34,9 @@ let row ~rite d = in String.concat "," (List.map escape_field - [ s d "iso"; rite; s d "season"; s d "week"; s d "slug"; s d "rank"; - s d "colour"; s d "subject"; nested d "name" "la"; nested d "name" "en"; - s d "first"; s d "gospel"; comms ]) + [ s d "iso"; rite; s d "season"; s d "season_name"; s d "week"; s d "slug"; + s d "name"; s d "weekday"; s d "rank"; s d "rank_name"; s d "colour"; + s d "colour_name"; s d "subject"; s d "first"; s d "gospel"; comms ]) let year v = let rite = s v "rite" in diff --git a/lib/render/emit_ics.ml b/lib/render/emit_ics.ml index 41efb95..4e06eae 100644 --- a/lib/render/emit_ics.ml +++ b/lib/render/emit_ics.ml @@ -55,14 +55,10 @@ let line b l = Buffer.add_string b (Escape.fold_ics l) let event b ~rite ~dtstamp d = let iso = s d "iso" in if iso <> "" then begin - let name = - match get d "name" with - | Some (T.Obj kvs) -> ( - match List.assoc_opt "en" kvs with - | Some (T.Str x) when x <> "" -> x - | _ -> ( match List.assoc_opt "la" kvs with Some (T.Str x) -> x | _ -> s d "slug")) - | _ -> s d "slug" - in + (* [name] is the view's own resolved display string (view.ml) -- no + further fallback needed here: under [Lang.raw] it already equals + [slug], which is what a miss used to require picking by hand. *) + let name = s d "name" in let summary = Printf.sprintf "%s (%s, %s)" name (s d "rank") (s d "colour") in let desc = String.concat "\n" diff --git a/lib/render/emit_xml.ml b/lib/render/emit_xml.ml index 4e4b51a..3f1ca32 100644 --- a/lib/render/emit_xml.ml +++ b/lib/render/emit_xml.ml @@ -11,20 +11,19 @@ let el b name value = let day b d = Buffer.add_string b (" <day date=\"" ^ escape (s d "iso") ^ "\">\n"); el b "season" (s d "season"); + el b "season_name" (s d "season_name"); el b "week" (s d "week"); el b "slug" (s d "slug"); + (* The resolved display name. A single element, no [lang] attribute: the + view's own [name] is now one resolved string, not a lang-keyed object + (view.ml), so there is exactly one to emit rather than one per language. *) + el b "name" (s d "name"); + el b "weekday" (s d "weekday"); el b "rank" (s d "rank"); + el b "rank_name" (s d "rank_name"); el b "colour" (s d "colour"); + el b "colour_name" (s d "colour_name"); el b "subject" (s d "subject"); - (match get d "name" with - | Some (T.Obj kvs) -> - List.iter - (fun (lang, v) -> - match v with - | T.Str x -> Buffer.add_string b (" <name lang=\"" ^ escape lang ^ "\">" ^ escape x ^ "</name>\n") - | _ -> ()) - kvs - | _ -> ()); (match get d "comms" with | Some (T.List l) -> List.iter (fun c -> Buffer.add_string b (" <commemoration>" ^ escape (s c "slug") ^ "</commemoration>\n")) l diff --git a/lib/render/view.ml b/lib/render/view.ml index 733aa45..bbb8c7d 100644 --- a/lib/render/view.ml +++ b/lib/render/view.ml @@ -1,18 +1,10 @@ module K = Colitur_kernel module T = Template +module Lang = Colitur_naming.Lang let str s = T.Str s let bool b = T.Bool b -let month_names = - [| ("Ianuarius", "January"); ("Februarius", "February"); ("Martius", "March"); - ("Aprilis", "April"); ("Maius", "May"); ("Iunius", "June"); - ("Iulius", "July"); ("Augustus", "August"); ("September", "September"); - ("October", "October"); ("November", "November"); ("December", "December") |] - -let names_value (n : K.Names.t) = - T.Obj (List.map (fun (l, s) -> (K.Lang.to_string l, str s)) (K.Names.to_list n)) - let dow_int = function | K.Date.Sun -> 0 | K.Date.Mon -> 1 | K.Date.Tue -> 2 | K.Date.Wed -> 3 | K.Date.Thu -> 4 | K.Date.Fri -> 5 | K.Date.Sat -> 6 @@ -24,55 +16,78 @@ let citation_ref cits part = | Some c -> c.K.Citation.reference | None -> "" -let comm_value (c, priv) = +(* A commemoration's own name resolves through the SAME [lang.celebration] + table as the observed day's -- a commemoration's slug is drawn from the + identical sanctoral/temporal pool, not a second vocabulary. Templates + (the ordo booklet, Task 8) interpolate this as a plain [{{name}}] inside + [{{#comms}}], so it must be a string here too, not the kernel's own + lang-keyed [Celebration.names] object -- the same reasoning [day_value]'s + own [name] follows below, applied consistently rather than left as a + second, differently-shaped name field a template author would have to + remember. *) +let comm_value ~lang (c, priv) = + let slug_s = K.Slug.to_string c.K.Celebration.slug in T.Obj - [ ("slug", str (K.Slug.to_string c.K.Celebration.slug)); - ("name", names_value c.K.Celebration.names); + [ ("slug", str slug_s); + ("name", str (Lang.celebration lang slug_s)); ("privileged", bool (priv = K.Precedence.Privileged)) ] (* A padding cell: present so a grid row always has seven entries, and flagged so a template can render it blank. Every field a real day has is present and - empty, so a template never hits a missing key on a padding cell. *) + empty, so a template never hits a missing key on a padding cell -- the same + key SET as [day_value], not merely the same shape by coincidence. *) let padding_cell dow = T.Obj [ ("iso", str ""); ("dom", str ""); ("dow", str (string_of_int dow)); ("in_month", bool false); - ("season", str ""); ("week", str ""); ("slug", str ""); - ("name", T.Obj []); ("rank", str ""); - ("colour", str ""); + ("season", str ""); ("season_name", str ""); + ("week", str ""); ("slug", str ""); + ("name", str ""); ("weekday", str ""); + ("rank", str ""); ("rank_name", str ""); + ("colour", str ""); ("colour_name", str ""); ("is_white", bool false); ("is_red", bool false); ("is_green", bool false); ("is_violet", bool false); ("is_rose", bool false); ("is_black", bool false); ("subject", str ""); ("comms", T.List []); ("transferred_in", T.List []); ("transferred_out", T.List []); ("first", str ""); ("gospel", str ""); ("last", bool false) ] -let day_value ~vocab (d : ('s, 'r) K.Liturgical_day.t) = +let day_value ~lang ~vocab (d : ('s, 'r) K.Liturgical_day.t) = let date = d.K.Liturgical_day.date in let tmp = d.K.Liturgical_day.temporal in let cel = d.K.Liturgical_day.observed in let colour = cel.K.Celebration.colour in let is c = bool (colour = c) in + let slug_s = K.Slug.to_string cel.K.Celebration.slug in + let rank_s = vocab.K.Vocab.rank_to_string cel.K.Celebration.rank in + let colour_s = K.Colour.to_string colour in + let season_s = vocab.K.Vocab.season_to_string tmp.K.Temporal.season in T.Obj [ ("iso", str (K.Date.to_iso8601 date)); ("dom", str (string_of_int (K.Date.day date))); ("dow", str (string_of_int (dow_int (K.Date.weekday date)))); ("in_month", bool true); - ("season", str (vocab.K.Vocab.season_to_string tmp.K.Temporal.season)); + ("season", str season_s); + ("season_name", str (Lang.season lang season_s)); ("week", str (match tmp.K.Temporal.week with Some w -> string_of_int w | None -> "")); - ("slug", str (K.Slug.to_string cel.K.Celebration.slug)); - ("name", names_value cel.K.Celebration.names); - (* [rank] is the kernel's own class string ("class-1"); there is - deliberately no separate localized rank label here -- the kernel - has no per-language rank names to draw one from, and a field - whose contents cannot honestly differ from [name] should not - exist just to exist. Do not re-add one until the kernel can. *) - ("rank", str (vocab.K.Vocab.rank_to_string cel.K.Celebration.rank)); - ("colour", str (K.Colour.to_string colour)); + ("slug", str slug_s); + (* The resolved display name. A plain string, not a lang-keyed object: + a dotted {{name.la}} used to fall back WHOLESALE to the enclosing + month's own name.la and print "Ianuarius" on unnamed days. One + string removes that hazard entirely -- there is no dotted path left + for a partial match to climb out of. Under [Lang.raw] this equals + [slug] exactly (every lookup in the identity table echoes its key), + which is what makes [--raw] output byte-stable. *) + ("name", str (Lang.celebration lang slug_s)); + ("weekday", str (Lang.weekday lang (dow_int (K.Date.weekday date)))); + ("rank", str rank_s); + ("rank_name", str (Lang.rank lang rank_s)); + ("colour", str colour_s); + ("colour_name", str (Lang.colour lang colour_s)); ("is_white", is K.Colour.White); ("is_red", is K.Colour.Red); ("is_green", is K.Colour.Green); ("is_violet", is K.Colour.Violet); ("is_rose", is K.Colour.Rose); ("is_black", is K.Colour.Black); ("subject", str (K.Subject.to_string cel.K.Celebration.subject)); - ("comms", T.List (List.map comm_value d.K.Liturgical_day.commemorations)); + ("comms", T.List (List.map (comm_value ~lang) d.K.Liturgical_day.commemorations)); ( "transferred_in", T.List (match d.K.Liturgical_day.transferred_in with @@ -102,7 +117,14 @@ let set_last cells = (fun i c -> match c with T.Obj kvs -> T.Obj (("last", bool (i = 6)) :: List.remove_assoc "last" kvs) | v -> v) cells -let weeks_of_month ~first_dow day_values = +(* [month_num]/[month_name] are carried onto every WEEK object because the + engine has no {{../}} parent-path syntax: a nested {{num}} inside a week + silently finds the WEEK's own number, never the month's, so a template + that needs the month (the ordo booklet, Task 8, whose weeks span a + {{#months}}{{#weeks}} nesting) has no other way to reach it. Shaping the + data here, rather than inventing template syntax, is the same call the + [last] flag above already made. *) +let weeks_of_month ~first_dow ~month_num ~month_name day_values = let lead = List.init first_dow (fun i -> padding_cell i) in let cells = lead @ day_values in let rec chunk acc = function @@ -118,11 +140,35 @@ let weeks_of_month ~first_dow day_values = chunk (week :: acc) tl in List.mapi - (fun i w -> T.Obj [ ("num", str (string_of_int (i + 1))); ("days", T.List (set_last w)) ]) + (fun i w -> + T.Obj + [ ("num", str (string_of_int (i + 1))); + ("month_num", str month_num); + ("month_name", str month_name); + ("days", T.List (set_last w)) ]) (chunk [] cells) -let of_days ~vocab ~rite ~year days = - let dvs = List.map (fun d -> (d, day_value ~vocab d)) days in +(* The [term] vocabulary a template routes every fixed string through + ({{term.epistle}}, {{term.week}}, ...) so a translated booklet needs no + template edit. The key list is the vocabulary's own fixed, closed set + (lang/*.ini's own [term] section, Task 1/3) -- not open like [celebration], + so it is named here rather than invented a second time from a wildcard + enumeration. *) +let term_keys = [ "ordo"; "contents"; "epistle"; "lesson"; "gospel"; "commemoration"; "week" ] + +let term_value lang = T.Obj (List.map (fun k -> (k, str (Lang.term lang k))) term_keys) + +(* A localised grid header row: {name; last} objects, not bare strings -- + the engine rejects an empty tag path ({{.}}) as a parse error, so a + template walking this list needs a named field to interpolate. Always + Sunday-first (index 0), matching [padding_cell]'s own [dow] numbering and + every week this view builds. *) +let weekday_headings lang = + T.List + (List.init 7 (fun i -> T.Obj [ ("name", str (Lang.weekday lang i)); ("last", bool (i = 6)) ])) + +let of_days ~lang ~vocab ~rite ~year days = + let dvs = List.map (fun d -> (d, day_value ~lang ~vocab d)) days in let months = List.init 12 (fun i -> let m = i + 1 in @@ -135,15 +181,18 @@ let of_days ~vocab ~rite ~year days = | (d, _) :: _ -> dow_int (K.Date.weekday d.K.Liturgical_day.date) | [] -> 0 in - let la, en = month_names.(i) in + let month_num = string_of_int m in + let month_name = Lang.month lang m in T.Obj - [ ("num", str (string_of_int m)); - ("name", T.Obj [ ("la", str la); ("en", str en) ]); + [ ("num", str month_num); + ("name", str month_name); ("days", T.List day_values); - ("weeks", T.List (weeks_of_month ~first_dow day_values)) ]) + ("weeks", T.List (weeks_of_month ~first_dow ~month_num ~month_name day_values)) ]) in T.Obj [ ("rite", str rite); ("year", str (string_of_int year)); + ("term", term_value lang); + ("weekday_headings", weekday_headings lang); ("months", T.List months); ("days", T.List (List.map snd dvs)) ] diff --git a/lib/render/view.mli b/lib/render/view.mli index fb48590..81bfc26 100644 --- a/lib/render/view.mli +++ b/lib/render/view.mli @@ -15,10 +15,24 @@ different colour expression. Exactly one of the six is true on every day. *) val of_days : + lang:Colitur_naming.Lang.t -> vocab:('s, 'r) Colitur_kernel.Vocab.t -> rite:string -> year:int -> ('s, 'r) Colitur_kernel.Liturgical_day.t list -> Template.value -(** [of_days ~vocab ~rite ~year days] where [days] is one civil year, 1 January - to 31 December, in order. Pure and total. *) +(** [of_days ~lang ~vocab ~rite ~year days] where [days] is one civil year, + 1 January to 31 December, in order. Pure and total. + + [lang] resolves every display string -- a day's [name], its localised + [weekday]/[rank_name]/[colour_name]/[season_name], each month's [name], + and the top-level [term] vocabulary and [weekday_headings]. [name] is a + PLAIN STRING, not the old lang-keyed object: a dotted [{{name.la}}] + reference that misses falls back WHOLESALE to the enclosing scope (a + month's own [name.la]), which is how a printed booklet came to show + "Ianuarius" in place of a feast with no Latin name. A plain string has no + dotted path to fall back through, so that hazard is unrepresentable. + [slug] is untouched by [lang] -- it stays the stable machine key, + identical between any two calls that differ only in [lang]. Pass + {!Colitur_naming.Lang.raw} for the pre-naming behaviour, under which + [name] equals [slug] exactly (this is what CLI [--raw] uses). *) |
