aboutsummaryrefslogtreecommitdiff
path: root/lib/render/view.ml
blob: bedf998ed5043a45d18594650e5e950e2dae8ada (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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 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

let citation_ref cits part =
  match
    List.find_opt (fun (c : K.Citation.t) -> c.K.Citation.part = part) cits
  with
  | Some c -> c.K.Citation.reference
  | None -> ""

(* 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 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 -- 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 ""); ("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 ~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 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 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 ~lang) d.K.Liturgical_day.commemorations));
      ( "transferred_in",
        T.List
          (match d.K.Liturgical_day.transferred_in with
           | None -> []
           | Some c -> [ T.Obj [ ("slug", str (K.Slug.to_string c.K.Celebration.slug)) ] ]) );
      ( "transferred_out",
        T.List
          (List.map
             (fun (c, dest) ->
               T.Obj
                 [ ("slug", str (K.Slug.to_string c.K.Celebration.slug));
                   ("to", str (K.Date.to_iso8601 dest)) ])
             d.K.Liturgical_day.transferred_out) );
      ("first", str (citation_ref d.K.Liturgical_day.citations K.Citation.First));
      ("gospel", str (citation_ref d.K.Liturgical_day.citations K.Citation.Gospel));
      (* overwritten per grid row by [set_last]; false in the flat [days] list *)
      ("last", bool false) ]

(* Bucket a month's day values into Sunday-started weeks of exactly seven cells,
   padding both ends. This is the computation the template cannot do. *)
(* [last] is true on the seventh cell of a row. A table row needs its separator
   BETWEEN cells and the engine has no "unless last" construct, so the flag is
   data -- the same rule as [in_month]. Without it the LaTeX grid emits eight
   columns for seven cells and pdflatex rejects the file. *)
let set_last cells =
  List.mapi
    (fun i c -> match c with T.Obj kvs -> T.Obj (("last", bool (i = 6)) :: List.remove_assoc "last" kvs) | v -> v)
    cells

(* [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
    | [] -> List.rev acc
    | rest ->
        let take = min 7 (List.length rest) in
        let week = List.filteri (fun i _ -> i < take) rest in
        let tl = List.filteri (fun i _ -> i >= take) rest in
        let week =
          if take = 7 then week
          else week @ List.init (7 - take) (fun i -> padding_cell (take + i))
        in
        chunk (week :: acc) tl
  in
  List.mapi
    (fun i w ->
      T.Obj
        [ ("num", str (string_of_int (i + 1)));
          ("month_num", str month_num);
          ("month_name", str month_name);
          (* True on the month's own first week -- Defect 2 (the continuous
             ordo booklet): with the per-week page break gone, a template
             needs SOME signal to print a stronger, standalone month banner
             at the point a new month begins, rather than repeating the
             (unremarkable) "Month . Week N" header run-on run-on. The same
             "cheap flag beats invented template logic" call [last] above
             already made -- there is still no {{../}} parent-path syntax
             to test "is this month's first week" any other way. *)
          ("first", bool (i = 0));
          ("days", T.List (set_last w)) ])
    (chunk [] cells)

(* 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
        let own =
          List.filter (fun (d, _) -> K.Date.month d.K.Liturgical_day.date = m) dvs
        in
        let day_values = List.map snd own in
        let first_dow =
          match own with
          | (d, _) :: _ -> dow_int (K.Date.weekday d.K.Liturgical_day.date)
          | [] -> 0
        in
        let month_num = string_of_int m in
        let month_name = Lang.month lang m in
        T.Obj
          [ ("num", str month_num);
            ("name", str month_name);
            ("days", T.List day_values);
            ("weeks", T.List (weeks_of_month ~first_dow ~month_num ~month_name day_values)) ])
  in
  T.Obj
    [ ("rite", str rite);
      (* The reader-facing display name (lang/*.ini's own [rite] section,
         Defect 1) -- [rite] itself stays the stable internal key, exactly
         as [slug] is kept beside [name] on a day/commemoration. Every
         template that used to print the bare id ("ef") now prints this
         field instead. *)
      ("rite_name", str (Lang.rite lang 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)) ]