(* 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 show_num (v : Parse.verse_num) = string_of_int v.Parse.n ^ v.Parse.suffix in (* A range's [last] normally renders as a bare verse number. When it names its own, later chapter ([verse_end.chapter], parse.mli's own citation), it is rendered through the SAME [chapter_verse] template [one_part] below uses for the part's own leading "chapter:verses" -- so a style that reconfigures the chapter/verse separator (the "missal convention" comma, {!Colitur_citation.Render}'s own doc comment) renders a cross-chapter jump in that identical convention, not a hardcoded ":". *) let one_range (r : Parse.verse_range) = match r.Parse.last with | None -> show_num r.Parse.first | Some (l : Parse.verse_end) -> let last_str = match l.Parse.chapter with | None -> show_num l.Parse.verse | Some c -> subst st.chapter_verse [ ("chapter", string_of_int c); ("chapter_roman", roman_numeral c); ("verses", show_num l.Parse.verse) ] in subst st.range [ ("first", show_num r.Parse.first); ("last", last_str) ] 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 }