diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/citation/book.ml | 104 | ||||
| -rw-r--r-- | lib/citation/book.mli | 79 | ||||
| -rw-r--r-- | lib/citation/dune | 2 | ||||
| -rw-r--r-- | lib/citation/parse.ml | 110 | ||||
| -rw-r--r-- | lib/citation/parse.mli | 15 | ||||
| -rw-r--r-- | lib/citation/render.ml | 108 | ||||
| -rw-r--r-- | lib/citation/render.mli | 58 | ||||
| -rw-r--r-- | lib/citation/sigla.ml | 22 | ||||
| -rw-r--r-- | lib/citation/sigla.mli | 30 | ||||
| -rw-r--r-- | lib/naming/config.ml | 13 | ||||
| -rw-r--r-- | lib/naming/config.mli | 24 | ||||
| -rw-r--r-- | lib/naming/lang.ml | 24 | ||||
| -rw-r--r-- | lib/naming/lang.mli | 27 | ||||
| -rw-r--r-- | lib/render/dune | 5 | ||||
| -rw-r--r-- | lib/render/view.ml | 18 | ||||
| -rw-r--r-- | lib/render/view.mli | 14 |
16 files changed, 637 insertions, 16 deletions
diff --git a/lib/citation/book.ml b/lib/citation/book.ml new file mode 100644 index 0000000..2a2b9c0 --- /dev/null +++ b/lib/citation/book.ml @@ -0,0 +1,104 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +type id = string + +let to_string t = t + +(* Every book cited across the shipped EF data (lectionary, sanctoral + propers, and commons), with every spelling any of the three files uses. + The dotted/undotted and modern/Vulgate pairs are inherited from lectio -- + see book.mli. Surveyed directly against the data, all three files, not + the lectionary alone -- sanctoral.sexp alone carries more citations than + the lectionary and was the source of every token missed in the first + pass. *) +let table = + [ ("genesis", [ "Gen" ]); + ("exodus", [ "Ex"; "Exod" ]); + ("leviticus", [ "Lev" ]); + ("numbers", [ "Num" ]); + ("kings_3", [ "3 Kings"; "3 Kgs." ]); + ("kings_4", [ "4 Kings" ]); + ("esdras_2", [ "2 Esd." ]); + ("tobit", [ "Tob" ]); + ("judith", [ "Judith" ]); + ("esther", [ "Esther" ]); + ("proverbs", [ "Prov" ]); + ("song_of_songs", [ "Song" ]); + ("wisdom", [ "Wis"; "Wis." ]); + ("ecclesiasticus", [ "Ecclus"; "Sir"; "Eccli" ]); + ("isaiah", [ "Isa"; "Isa." ]); + ("jeremiah", [ "Jer" ]); + ("ezekiel", [ "Ezech"; "Ezek" ]); + ("daniel", [ "Dan" ]); + ("osee", [ "Osee" ]); + ("joel", [ "Joel" ]); + ("jonas", [ "Jonas" ]); + ("malachi", [ "Mal" ]); + ("matthew", [ "Matt"; "Matt." ]); + ("mark", [ "Mark" ]); + ("luke", [ "Luke" ]); + ("john", [ "John" ]); + ("acts", [ "Acts" ]); + ("romans", [ "Rom" ]); + ("corinthians_1", [ "1 Cor"; "1 Cor." ]); + ("corinthians_2", [ "2 Cor"; "2 Cor." ]); + ("galatians", [ "Gal" ]); + ("ephesians", [ "Eph"; "Eph." ]); + ("philippians", [ "Phil" ]); + ("colossians", [ "Col"; "Col." ]); + ("thessalonians_1", [ "1 Thess"; "1 Thess." ]); + ("thessalonians_2", [ "2 Thess" ]); + ("timothy_1", [ "1 Tim." ]); + ("timothy_2", [ "2 Tim"; "2 Tim." ]); + ("titus", [ "Titus" ]); + ("hebrews", [ "Heb" ]); + ("james", [ "Jas"; "James" ]); + ("peter_1", [ "1 Pet"; "1 Pet." ]); + ("peter_2", [ "2 Pet." ]); + ("john_1", [ "1 John" ]); + (* "Apoc" is the Vulgate spelling and "Rev" its modern equivalent, but + BOTH sit inside Vulgate-tradition data, so both resolve to the same + Vulgate id here -- see book.mli's note by [apocalypse] never being an + [of_token] result under that name. Do not add "revelation" as a + spelling: it exists only as a tradition target (below), and giving it + an [of_token] entry would let one book carry two different ids. *) + ("apocalypse", [ "Apoc"; "Rev" ]) ] + +(* Targets a tradition can map ONTO that the Vulgate data never cites + directly. Present so [tradition_of_fields] can validate both sides. + [sirach] and [revelation] exist ONLY here, never as an [of_token] result: + "Sir" and "Rev" already resolve to the Vulgate ids [ecclesiasticus] and + [apocalypse] above, so a modern-numbering tradition maps ONTO these + targets rather than data ever citing them directly. *) +let tradition_targets = + [ "kings_1"; "kings_2"; "nehemiah"; "sirach"; "hosea"; "jonah"; "revelation" ] + +let all = List.map fst table @ tradition_targets + +let tokens = + List.concat_map (fun (id, sp) -> List.map (fun s -> (s, id)) sp) table + +let default_spelling id = + match List.assoc_opt id table with + | Some (first :: _) -> first + | Some [] | None -> id + +let of_token s = + let s = String.trim s in + List.assoc_opt s tokens + +type tradition = (string * string) list + +let vulgate = [] + +let known id = List.mem id all + +let tradition_of_fields fields = + List.filter (fun (a, b) -> known a && known b) fields + +let unknown_fields fields = + List.filter_map + (fun (a, b) -> if known a && known b then None else Some a) + fields + +let map tr id = match List.assoc_opt id tr with Some x -> x | None -> id diff --git a/lib/citation/book.mli b/lib/citation/book.mli new file mode 100644 index 0000000..0ca229c --- /dev/null +++ b/lib/citation/book.mli @@ -0,0 +1,79 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +(** Bible books: one canonical id per book, and the tradition that decides + which book an id denotes. + + Ids follow the VULGATE structure ([kings_3], [esdras_2], + [ecclesiasticus]), because the Vulgate is the default tradition and the + shipped 1962 data is Vulgate throughout. An id is internal -- it is never + shown to a reader, exactly as a slug is never shown. *) + +type id + +val to_string : id -> string + +(** Resolve one spelling as it appears in the data. Returns [None] for + anything not in {!tokens}. + + SIXTEEN books arrive in more than one spelling, surveyed across all + three citation-bearing files ([lectionary.sexp], [sanctoral.sexp], + [commons.sexp] -- not the lectionary alone, which undercounts: the + sanctoral propers alone carry more citations than the lectionary does). + Most are a dotted/undotted pair ([Isa]/[Isa.], [3 Kgs.]/[3 Kings], and + others); two also carry a MODERN spelling sitting inside otherwise- + Vulgate data ([Sir] alongside [Ecclus]/[Eccli], [Rev] alongside [Apoc]). + All of this is INHERITED from lectio's own ini, which is itself + generated from missalemeum/Divinum Officium -- it is not a colitur + transcription error, and the data is deliberately left untouched. Every + accepted spelling resolves here to the same, single Vulgate id: [Sir] + resolves to [ecclesiasticus] and [Rev] to [apocalypse], never to the + tradition-only targets [sirach]/[revelation] -- see those below. *) +val of_token : string -> id option + +(** Every id this build knows, for coverage checks. *) +val all : id list + +(** Every accepted spelling paired with its id. *) +val tokens : (string * id) list + +(** The first spelling registered for an id -- the form the shipped data + itself uses ([luke] -> ["Luke"], [kings_3] -> ["3 Kings"]). + + This is the FALLBACK display name, and it exists because the obvious + alternative is actively worse. A language file's [\[bible\]] lookup is + total and returns THE KEY on a miss, so a book with no entry would + otherwise render as ["luke.abbr 5:12-14"]. Falling back here instead makes + an unnamed book render as ["Luke 5:12-14"] -- exactly what colitur printed + before this feature existed. The degraded case is the OLD behaviour, not a + broken page, the same principle {!Colitur_naming.Lang} states for its own + key-returning misses. + + An id with no registered spelling -- only a {!tradition} target such as + [kings_1] or [sirach], which the Vulgate data never cites -- returns the id + itself. Reachable only from a user language file that selects a tradition + without naming its target books; every SHIPPED language file is asserted + complete over {!all}. *) +val default_spelling : id -> string + +(** A numbering tradition: which book an id denotes. Separate from NAMING + (what a book is called), which lives in a language file's [\[bible\]] + section, because naming varies by language and this does not -- "modern + numbering" is the same decision in Latin, Polish and English. *) +type tradition + +(** The identity tradition: the Vulgate, as the 1962 Missal prints it. The + default; colitur never silently renumbers. *) +val vulgate : tradition + +(** Build a tradition from an ini section's fields. An entry naming an + unknown id on either side is IGNORED, not fatal: a traditions file + written for a newer colitur must still work on an older one. Use + {!unknown_fields} to report them. *) +val tradition_of_fields : (string * string) list -> tradition + +(** The fields {!tradition_of_fields} silently dropped -- either side naming + an id outside {!all}. Never raises; a caller that cares can report these, + a caller that does not can ignore the return value entirely. *) +val unknown_fields : (string * string) list -> string list + +val map : tradition -> id -> id diff --git a/lib/citation/dune b/lib/citation/dune new file mode 100644 index 0000000..e6cec3c --- /dev/null +++ b/lib/citation/dune @@ -0,0 +1,2 @@ +(library + (name colitur_citation)) diff --git a/lib/citation/parse.ml b/lib/citation/parse.ml new file mode 100644 index 0000000..45fbb72 --- /dev/null +++ b/lib/citation/parse.ml @@ -0,0 +1,110 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +type verse_range = { first : int; last : int option } +type part = { chapter : int; verses : verse_range list } +type t = { book : Book.id; parts : part list } + +let split_on c s = String.split_on_char c s |> List.map String.trim + +(* The book is the longest leading run of non-digit words, allowing one + leading ordinal ("1 Cor", "3 Kings"). Everything after it is the + reference tail. *) +let split_book s = + let n = String.length s in + let i = ref 0 in + (* optional leading ordinal digit *) + if !i < n && s.[!i] >= '1' && s.[!i] <= '4' then begin + incr i; + while !i < n && s.[!i] = ' ' do incr i done + end; + (* letters and dots *) + while !i < n && (s.[!i] = '.' || (s.[!i] >= 'A' && s.[!i] <= 'z')) do incr i done; + if !i = 0 then None + else + let book = String.trim (String.sub s 0 !i) in + let tail = String.trim (String.sub s !i (n - !i)) in + if book = "" || tail = "" then None else Some (book, tail) + +let int_opt s = int_of_string_opt (String.trim s) + +(* "20-32" -> {first=20; last=Some 32}; "21" -> {first=21; last=None} *) +let parse_range s = + match split_on '-' s with + | [ a ] -> ( match int_opt a with Some f -> Some { first = f; last = None } | None -> None) + | [ a; b ] -> ( + match (int_opt a, int_opt b) with + | Some f, Some l -> Some { first = f; last = Some l } + | _ -> None) + | _ -> None + +let parse_ranges s = + let pieces = split_on ',' s in + List.fold_right + (fun p acc -> + match (parse_range p, acc) with + | Some r, Some rest -> Some (r :: rest) + | _ -> None) + pieces (Some []) + +(* One ";"-separated part. [inherited] is the chapter of the previous part, + used when this one names none (rule 1 in the grammar table). *) +let parse_part ~inherited s = + match split_on ':' s with + | [ c; v ] -> ( + (* explicit "chapter:verses" *) + match (int_opt c, parse_ranges v) with + | Some ch, Some vs -> Some { chapter = ch; verses = vs } + | _ -> None) + | [ only ] -> ( + (* Either "chapter, verses" (rule 3) or bare verses inheriting a + chapter. Distinguish on whether the FIRST comma-piece is a lone + number followed by more pieces -- a leading number followed by + at least one further piece is a chapter introduction ("15, 1-46"); + a lone piece on its own, or a part with no more pieces to follow, + can only be verses inheriting the previous chapter. *) + let pieces = split_on ',' only in + match (pieces, inherited) with + | first :: (_ :: _ as rest), _ when int_opt first <> None && String.contains only ',' -> ( + (* "15, 1-46" -> chapter 15. This never fires for a part that + already named its chapter via ":" -- those match the [c; v] + branch above and never reach here. *) + match (int_opt first, parse_ranges (String.concat "," rest)) with + | Some ch, Some vs -> Some { chapter = ch; verses = vs } + | _ -> None) + | _, Some ch -> ( + match parse_ranges only with + | Some vs -> Some { chapter = ch; verses = vs } + | None -> None) + | _, None -> None) + | _ -> None + +let parse s = + let s = String.trim s in + (* A trailing period is decoration, not data: 22 citations carry one. *) + let s = + let n = String.length s in + if n > 0 && s.[n - 1] = '.' then String.sub s 0 (n - 1) else s + in + match split_book s with + | None -> Error "no book" + | Some (btok, tail) -> ( + match Book.of_token btok with + | None -> Error ("unknown book: " ^ btok) + | Some book -> + (* A trailing ";" leaves an empty piece: drop it rather than + failing. Only if nothing remains is it an error. *) + let pieces = List.filter (fun p -> p <> "") (split_on ';' tail) in + let rec go inherited = function + | [] -> Ok [] + | p :: rest -> ( + match parse_part ~inherited p with + | None -> Error ("cannot read reference: " ^ p) + | Some part -> ( + match go (Some part.chapter) rest with + | Error e -> Error e + | Ok more -> Ok (part :: more))) + in + (match go None pieces with + | Error e -> Error e + | Ok [] -> Error "empty reference" + | Ok parts -> Ok { book; parts })) diff --git a/lib/citation/parse.mli b/lib/citation/parse.mli new file mode 100644 index 0000000..3d1053c --- /dev/null +++ b/lib/citation/parse.mli @@ -0,0 +1,15 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +(** A citation, parsed. Never raises; an unrecognised string is an [Error] + naming what could not be read, never a silent pass-through. + + The shape is a book and a LIST of chapter-parts, not one chapter and one + verse range, because the shipped data really does cite across chapters + ([John 18:1-40; 19:1-42]) and really does list disjoint verse ranges + within a chapter ([Dan 13:1-9, 15-17, 19-30, 33-62]). *) + +type verse_range = { first : int; last : int option } +type part = { chapter : int; verses : verse_range list } +type t = { book : Book.id; parts : part list } + +val parse : string -> (t, string) result diff --git a/lib/citation/render.ml b/lib/citation/render.ml new file mode 100644 index 0000000..79aaa86 --- /dev/null +++ b/lib/citation/render.ml @@ -0,0 +1,108 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +type style = { + book : [ `Full | `Abbr ]; + book_sep : string; + chapter_verse : string; + range : string; + part_sep : string; + verse_sep : string; +} + +let default_style = + { book = `Abbr; + book_sep = " "; + chapter_verse = "{chapter}:{verses}"; + range = "{first}-{last}"; + part_sep = "; "; + verse_sep = ", " } + +let part_sep st = st.part_sep +let range st = st.range +let book_sep st = st.book_sep +let book_string st = match st.book with `Full -> "full" | `Abbr -> "abbr" + +(* Arabic -> Roman, for the {chapter_roman} placeholder. Lifted from + [Colitur_render.View.roman_numeral], which prints "Hebdomada I" week + headings -- roman numerals are idiomatic throughout this project's + output ("Feria IV"), so a citation style may want them too. *) +let roman_numeral n = + if n <= 0 then string_of_int n + else + let table = + [ (1000, "M"); (900, "CM"); (500, "D"); (400, "CD"); (100, "C"); + (90, "XC"); (50, "L"); (40, "XL"); (10, "X"); (9, "IX"); + (5, "V"); (4, "IV"); (1, "I") ] + in + let b = Buffer.create 8 in + let n = ref n in + List.iter + (fun (v, sym) -> + while !n >= v do + Buffer.add_string b sym; + n := !n - v + done) + table; + Buffer.contents b + +let with_book b st = { st with book = b } + +(* One matching pair of surrounding double quotes, and only that -- see the + .mli for why this is not in Overlay_ini. *) +let unquote s = + let n = String.length s in + if n >= 2 && s.[0] = '"' && s.[n - 1] = '"' then String.sub s 1 (n - 2) else s + +(* Replace {name} with its value. An UNKNOWN placeholder survives + literally: a typo in a hand-written style file must be visible in the + output, not silently swallowed. *) +let subst tmpl pairs = + let n = String.length tmpl in + let b = Buffer.create (n + 16) in + let i = ref 0 in + while !i < n do + if tmpl.[!i] = '{' then + match String.index_from_opt tmpl !i '}' with + | None -> + Buffer.add_char b tmpl.[!i]; + incr i + | Some j -> + let name = String.sub tmpl (!i + 1) (j - !i - 1) in + (match List.assoc_opt name pairs with + | Some v -> Buffer.add_string b v + | None -> Buffer.add_string b (String.sub tmpl !i (j - !i + 1))); + i := j + 1 + else begin + Buffer.add_char b tmpl.[!i]; + incr i + end + done; + Buffer.contents b + +let render st ~names (t : Parse.t) = + let one_range (r : Parse.verse_range) = + match r.Parse.last with + | None -> string_of_int r.Parse.first + | Some l -> + subst st.range + [ ("first", string_of_int r.Parse.first); + ("last", string_of_int l) ] + in + let one_part (p : Parse.part) = + let verses = String.concat st.verse_sep (List.map one_range p.Parse.verses) in + subst st.chapter_verse + [ ("chapter", string_of_int p.Parse.chapter); + ("chapter_roman", roman_numeral p.Parse.chapter); + ("verses", verses) ] + in + let body = String.concat st.part_sep (List.map one_part t.Parse.parts) in + names t.Parse.book st.book ^ st.book_sep ^ body + +let style_of_fields fields = + let get k d = match List.assoc_opt k fields with Some v -> unquote v | None -> d in + { book = (if get "book" "abbr" = "full" then `Full else `Abbr); + book_sep = get "book_sep" default_style.book_sep; + chapter_verse = get "chapter_verse" default_style.chapter_verse; + range = get "range" default_style.range; + part_sep = get "part_sep" default_style.part_sep; + verse_sep = get "verse_sep" default_style.verse_sep } diff --git a/lib/citation/render.mli b/lib/citation/render.mli new file mode 100644 index 0000000..274900c --- /dev/null +++ b/lib/citation/render.mli @@ -0,0 +1,58 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +(** Turn a parsed citation back into text, in a configurable style. + + A style is data, not code, so a tradition of punctuation is a file + someone can write: [Luke 5:12-14], [Lk 5:12-14], [Luke 5.12-14], + [{L}k 5, 12-14] are all the same citation in different conventions. + + Placeholders: [{chapter}], [{chapter_roman}] and [{verses}] in + [chapter_verse]; [{first}] and [{last}] in [range]. An UNKNOWN + placeholder survives literally, so a typo is visible in the output + rather than silently swallowed. *) + +type style + +(** The Vulgate/Latin convention: abbreviated book, [chapter:verses], + [first-last], ["; "] between parts, [", "] between verse ranges. *) +val default_style : style + +(** Read a style from a language file's [\[sigla\]] section. + + Values are UNQUOTED here: one matching pair of surrounding double quotes + is stripped, so a separator's significant trailing space survives. + {!Colitur_kernel.Overlay_ini} trims every value and has no quote + handling, and it is shared with overlays and [\[defaults\]] -- so the + unquoting belongs here, not there. An unrecognised key is ignored; + a missing key keeps {!default_style}'s value. *) +val style_of_fields : (string * string) list -> style + +(** Override the book form, for the [sigla_book] config key. *) +val with_book : [ `Full | `Abbr ] -> style -> style + +val part_sep : style -> string +val range : style -> string + +(** What separates the book name from the reference. Default [" "]. + + Settable because a typeset booklet wants a NON-BREAKING space here -- a + line break between "Luc." and "3, 1" is exactly the ugliness this + prevents. Set it to a literal U+00A0: the escapers match ASCII bytes + only, so a UTF-8 multibyte sequence passes through every flavour + untouched (verified for latex, typst, groff, html, xml, ics). A LaTeX + tie [~] does NOT work -- {!Colitur_render.Escape} turns it into + [\textasciitilde{}]. *) +val book_sep : style -> string + +(** The style's own book form, as the string a config file would write + (["full"] or ["abbr"]). + + Exists so a caller can use the STYLE's value as the default when + resolving the [sigla_book] setting. Resolving against a hardcoded + ["abbr"] instead makes a style file's own [book] key unreachable -- + the value is always overwritten before it can apply -- which is what + shipped until this accessor existed. *) +val book_string : style -> string + +val render : + style -> names:(Book.id -> [ `Full | `Abbr ] -> string) -> Parse.t -> string diff --git a/lib/citation/sigla.ml b/lib/citation/sigla.ml new file mode 100644 index 0000000..a4f4555 --- /dev/null +++ b/lib/citation/sigla.ml @@ -0,0 +1,22 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +type t = + | Verbatim + | Styled of { + style : Render.style; + tradition : Book.tradition; + names : Book.id -> [ `Full | `Abbr ] -> string; + } + +let verbatim = Verbatim +let make ~style ~tradition ~names = Styled { style; tradition; names } + +let format t s = + match t with + | Verbatim -> s + | Styled { style; tradition; names } -> ( + match Parse.parse s with + | Error _ -> s + | Ok c -> + let c = { c with Parse.book = Book.map tradition c.Parse.book } in + Render.render style ~names c) diff --git a/lib/citation/sigla.mli b/lib/citation/sigla.mli new file mode 100644 index 0000000..d21877a --- /dev/null +++ b/lib/citation/sigla.mli @@ -0,0 +1,30 @@ +(* SPDX-License-Identifier: AGPL-3.0-or-later *) + +(** The whole citation pipeline as one TOTAL function, so a call site needs + one value and one call rather than a parse/map/render dance. + + {!format} never raises and never drops text: a citation that does not + parse is returned UNCHANGED. That combination is deliberate -- graceful + in production, while [test_citation_coverage.ml] asserts strictly that + no shipped citation actually takes that path. *) + +type t + +val make : + style:Render.style -> + tradition:Book.tradition -> + names:(Book.id -> [ `Full | `Abbr ] -> string) -> + t + +(** Returns every citation exactly as given. This is what [--raw] uses, the + same shape as {!Colitur_naming.Lang.raw}: raw output is one value passed + around, not a special case threaded through every call site. + + [--raw] must NOT merely use an identity name table -- that would still + reformat punctuation and apply a tradition. Byte-exact output is what + makes [--raw] usable for diffing against lectio, and it also keeps the + raw view independent of the parser, so a parser bug cannot corrupt the + output used to diagnose it. *) +val verbatim : t + +val format : t -> string -> string diff --git a/lib/naming/config.ml b/lib/naming/config.ml index 7d87d62..068b40a 100644 --- a/lib/naming/config.ml +++ b/lib/naming/config.ml @@ -5,18 +5,24 @@ type t = { overlays : string list; template : string option; format : string option; + sigla_style : string option; + sigla_book : string option; + sigla_tradition : string option; unknown_keys : string list; unknown_sections : string list; } let empty = - { lang = None; overlays = []; template = None; format = None; unknown_keys = []; - unknown_sections = [] } + { lang = None; overlays = []; template = None; format = None; sigla_style = None; + sigla_book = None; sigla_tradition = None; unknown_keys = []; unknown_sections = [] } let lang t = t.lang let overlays t = t.overlays let template t = t.template let format t = t.format +let sigla_style t = t.sigla_style +let sigla_book t = t.sigla_book +let sigla_tradition t = t.sigla_tradition let unknown_keys t = t.unknown_keys let unknown_sections t = t.unknown_sections @@ -65,6 +71,9 @@ let of_string text = | "lang" -> { acc with lang = Some v } | "template" -> { acc with template = Some v } | "format" -> { acc with format = Some v } + | "sigla_style" -> { acc with sigla_style = Some v } + | "sigla_book" -> { acc with sigla_book = Some v } + | "sigla_tradition" -> { acc with sigla_tradition = Some v } (* accumulates: a user has more than one overlay *) | "overlay" -> { acc with overlays = v :: acc.overlays } | other -> { acc with unknown_keys = other :: acc.unknown_keys }) diff --git a/lib/naming/config.mli b/lib/naming/config.mli index a84f95b..a5205c4 100644 --- a/lib/naming/config.mli +++ b/lib/naming/config.mli @@ -29,6 +29,30 @@ val overlays : t -> string list val template : t -> string option val format : t -> string option +(** Which citation style to render a reference in: a language CODE, looked + up the same way {!lang} is, or a path -- but a DIFFERENT axis from + {!lang}: a language file's own [\[sigla\]] section IS a style + ({!Colitur_citation.Render.style_of_fields}), and this key SELECTS + which file's [\[sigla\]] section supplies it, independently of which + file's other sections supply names elsewhere (a booklet may want + Polish names but Latin-convention citations). Same last-wins duplicate + policy as {!lang}. *) +val sigla_style : t -> string option + +(** [full] or [abbr] -- overrides the style's own [book] setting rather than + replacing the style outright, so a chosen style's punctuation survives + even when the book form is overridden ({!Colitur_citation.Render.with_book}). + Same last-wins duplicate policy as {!lang}. *) +val sigla_book : t -> string option + +(** The name of a section in [lang/traditions.ini] -- which BOOK a reference + DENOTES (Vulgate numbering by default), a different question again from + both {!sigla_style} (how a reference is WRITTEN) and {!lang} (what + everything else is CALLED): naming and numbering both vary by + convention, but not together. Same last-wins duplicate policy as + {!lang}. *) +val sigla_tradition : t -> string option + (** Keys present in the [\[defaults\]] section that this build does not understand. Reported, never fatal: a config written for a newer colitur must still work on an older one, but silently ignoring a line the user diff --git a/lib/naming/lang.ml b/lib/naming/lang.ml index 2301513..f0fd989 100644 --- a/lib/naming/lang.ml +++ b/lib/naming/lang.ml @@ -16,6 +16,8 @@ type t = { colour : table; term : table; rite : table; + bible : table; + sigla : table; chain : t option; (** consulted when this table misses *) } @@ -35,6 +37,14 @@ let rank t k = get t (fun x -> x.rank) k let colour t k = get t (fun x -> x.colour) k let term t k = get t (fun x -> x.term) k let rite t k = get t (fun x -> x.rite) k +let bible t k = get t (fun x -> x.bible) k + +(* Not routed through [get]/[chain]: [sigla] is a set of RENDER SETTINGS + (Render.style_of_fields), not a per-key translation lookup, and it has no + miss-returns-the-key contract to keep -- a caller with no [sigla] table at + all just gets []. Order does not matter to callers (Render.style_of_fields + reads them by name), so [SM.bindings] (alphabetical) is fine here. *) +let sigla_fields t = SM.bindings t.sigla let weekday_key = [| "sunday"; "monday"; "tuesday"; "wednesday"; "thursday"; "friday"; "saturday" |] @@ -65,7 +75,8 @@ let fallback_code t = t.fallback_code let raw = { code = "raw"; fallback_code = None; celebration = empty_table; weekday = empty_table; month = empty_table; month_abbr = empty_table; season = empty_table; rank = empty_table; - colour = empty_table; term = empty_table; rite = empty_table; chain = None } + colour = empty_table; term = empty_table; rite = empty_table; bible = empty_table; + sigla = empty_table; chain = None } let with_fallback t base = { t with chain = Some base } @@ -107,12 +118,21 @@ let of_string text = colour = find "colour"; term = find "term"; rite = find "rite"; + bible = find "bible"; + sigla = find "sigla"; chain = None }) +(* [bible] joins this reference set: it is translatable book names, so a file + lacking them is genuinely incomplete and [lang --check] should say so. + [sigla] deliberately does NOT: it is five citation-style settings with + working defaults (Render.default_style), not names a translator owes -- + adding it here would make [--check] demand five settings from every + language file that has never needed them. *) let keys t = let qualify prefix m = SM.bindings m |> List.map (fun (k, v) -> (prefix ^ "." ^ k, v)) in List.concat [ qualify "celebration" t.celebration; qualify "weekday" t.weekday; qualify "month" t.month; qualify "month_abbr" t.month_abbr; qualify "season" t.season; - qualify "rank" t.rank; qualify "colour" t.colour; qualify "term" t.term; qualify "rite" t.rite ] + qualify "rank" t.rank; qualify "colour" t.colour; qualify "term" t.term; qualify "rite" t.rite; + qualify "bible" t.bible ] |> List.sort compare diff --git a/lib/naming/lang.mli b/lib/naming/lang.mli index 34a6d5d..3c62d26 100644 --- a/lib/naming/lang.mli +++ b/lib/naming/lang.mli @@ -55,6 +55,13 @@ val term : t -> string -> string degradation an unnamed slug already gets, not a special case. *) val rite : t -> string -> string +(** A Bible book's name, from the [\[bible\]] section. The key is + [<book_id>.full] or [<book_id>.abbr] (e.g. ["luke.abbr"]). A miss returns + the key itself, the same TOTAL-lookup contract as every other function + here -- callers turn that into a real fallback via + {!Colitur_citation.Book.default_spelling}, not this module. *) +val bible : t -> string -> string + (** [weekday t n], 0 = Sunday. Out-of-range [n] returns [string_of_int n]. *) val weekday : t -> int -> string @@ -69,5 +76,23 @@ val month : t -> int -> string val month_abbr : t -> int -> string (** Every (section-qualified key, value) pair, sorted. Used by [lang --dump] and - [lang --check]. Keys are qualified as e.g. ["celebration.ef-epiphany"]. *) + [lang --check]. Keys are qualified as e.g. ["celebration.ef-epiphany"]. + + Includes [\[bible\]] (["bible.luke.abbr"]): book names are translatable + text, so a file missing them is genuinely incomplete and [lang --check] + should say so. Deliberately EXCLUDES [\[sigla\]]: those are five citation- + style settings with working defaults ({!Colitur_citation.Render.default_style}), + not names a translator owes -- listing them here would make [--check] + demand five settings from every language file. *) val keys : t -> (string * string) list + +(** The [\[sigla\]] section's raw fields, for + {!Colitur_citation.Render.style_of_fields} to read. Values come back + TRIMMED (whitespace at both ends) but still QUOTED if the file quoted + them -- unquoting is [Render]'s job, not this module's, the same reason + {!Colitur_kernel.Overlay_ini} (which this parses through) never unquotes + either. An empty (or absent) [\[sigla\]] section returns [[]]; unlike + every lookup above, there is no per-key TOTAL contract to keep here -- + this is a settings bag, not a translation table, and [Render] already + supplies its own defaults for whatever is missing. *) +val sigla_fields : t -> (string * string) list diff --git a/lib/render/dune b/lib/render/dune index 548cf43..26e6d0d 100644 --- a/lib/render/dune +++ b/lib/render/dune @@ -1,5 +1,8 @@ (library (name colitur_render) - (libraries colitur_kernel colitur_naming sexplib) + ; [colitur_citation] backs [View.citation_ref]'s own [Sigla.format] call + ; (Task 9): a citation now renders through a caller-supplied style rather + ; than passing its stored reference straight through. + (libraries colitur_kernel colitur_naming colitur_citation sexplib) (preprocess (pps ppx_sexp_conv))) diff --git a/lib/render/view.ml b/lib/render/view.ml index 9f489aa..0691ffb 100644 --- a/lib/render/view.ml +++ b/lib/render/view.ml @@ -1,6 +1,7 @@ module K = Colitur_kernel module T = Template module Lang = Colitur_naming.Lang +module Sigla = Colitur_citation.Sigla let str s = T.Str s let bool b = T.Bool b @@ -43,11 +44,14 @@ 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 = +(* [sigla] renders the stored reference in the caller's chosen style + (Task 9) -- under [Sigla.verbatim] this is the identity, so [--raw]'s + own byte-exact contract is unaffected. *) +let citation_ref ~sigla cits part = match List.find_opt (fun (c : K.Citation.t) -> c.K.Citation.part = part) cits with - | Some c -> c.K.Citation.reference + | Some c -> Sigla.format sigla c.K.Citation.reference | None -> "" (* A commemoration's own name resolves through the SAME [lang.celebration] @@ -85,7 +89,7 @@ let padding_cell dow = ("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 day_value ~lang ~sigla ~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 @@ -135,8 +139,8 @@ let day_value ~lang ~vocab (d : ('s, 'r) K.Liturgical_day.t) = [ ("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)); + ("first", str (citation_ref ~sigla d.K.Liturgical_day.citations K.Citation.First)); + ("gospel", str (citation_ref ~sigla d.K.Liturgical_day.citations K.Citation.Gospel)); (* overwritten per grid row by [set_last]; false in the flat [days] list *) ("last", bool false) ] @@ -245,8 +249,8 @@ 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 of_days ~lang ~sigla ~vocab ~rite ~year days = + let dvs = List.map (fun d -> (d, day_value ~lang ~sigla ~vocab d)) days in let months = List.init 12 (fun i -> let m = i + 1 in diff --git a/lib/render/view.mli b/lib/render/view.mli index 81bfc26..d271641 100644 --- a/lib/render/view.mli +++ b/lib/render/view.mli @@ -16,13 +16,14 @@ val of_days : lang:Colitur_naming.Lang.t -> + sigla:Colitur_citation.Sigla.t -> vocab:('s, 'r) Colitur_kernel.Vocab.t -> rite:string -> year:int -> ('s, 'r) Colitur_kernel.Liturgical_day.t list -> Template.value -(** [of_days ~lang ~vocab ~rite ~year days] where [days] is one civil year, - 1 January to 31 December, in order. Pure and total. +(** [of_days ~lang ~sigla ~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], @@ -35,4 +36,11 @@ val of_days : [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). *) + [name] equals [slug] exactly (this is what CLI [--raw] uses). + + [sigla] resolves the [first]/[gospel] citation fields (Task 9): each + stored reference is passed through {!Colitur_citation.Sigla.format} + rather than emitted verbatim. Pass {!Colitur_citation.Sigla.verbatim} + alongside {!Colitur_naming.Lang.raw} for [--raw] -- a styled [Sigla.t] + built over [Lang.raw] would still parse and reformat every citation, + which defeats the byte-exact diffing [--raw] exists for. *) |
