summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/citation/book.ml104
-rw-r--r--lib/citation/book.mli79
-rw-r--r--lib/citation/dune2
-rw-r--r--lib/citation/parse.ml110
-rw-r--r--lib/citation/parse.mli15
-rw-r--r--lib/citation/render.ml108
-rw-r--r--lib/citation/render.mli58
-rw-r--r--lib/citation/sigla.ml22
-rw-r--r--lib/citation/sigla.mli30
-rw-r--r--lib/naming/config.ml13
-rw-r--r--lib/naming/config.mli24
-rw-r--r--lib/naming/lang.ml24
-rw-r--r--lib/naming/lang.mli27
-rw-r--r--lib/render/dune5
-rw-r--r--lib/render/view.ml18
-rw-r--r--lib/render/view.mli14
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. *)