(* 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"; "Liber Genesis"; "Genesis" ]); ("exodus", [ "Ex"; "Exod"; "Liber Exodi"; "Exodus" ]); ("leviticus", [ "Lev"; "Levit"; "Liber Levitici"; "Leviticus" ]); ("numbers", [ "Num"; "Liber Numeri"; "Numbers" ]); ("kings_3", [ "3 Kings"; "3 Kgs."; "3 Reg"; "Liber Regum III" ]); ("kings_4", [ "4 Kings"; "4 Reg"; "Liber Regum IV"; "4 Kgs." ]); ("esdras_2", [ "2 Esd."; "2 Esdr"; "Liber Esdrae"; "2 Esdras" ]); ("tobit", [ "Tob"; "Liber Tobiae"; "Tobias"; "Tobit" ]); ("judith", [ "Judith"; "Iudith"; "Liber Iudith"; "Jth" ]); ("esther", [ "Esther"; "Esth"; "Liber Esther" ]); ("proverbs", [ "Prov"; "Proverbs" ]); ("song_of_songs", [ "Song"; "Cant."; "Canticle of Canticles"; "Song of Solomon" ]); ("wisdom", [ "Wis"; "Wis."; "Sap"; "Liber Sapientiae"; "Wisdom" ]); ("ecclesiasticus", [ "Ecclus"; "Sir"; "Eccli"; "Ecclesiasticus" ]); ("isaiah", [ "Isa"; "Isa."; "Isai"; "Isaias Propheta"; "Isaias"; "Isaiah" ]); ("jeremiah", [ "Jer"; "Ier"; "Ieremias Propheta"; "Jeremias"; "Jeremiah" ]); ("ezekiel", [ "Ezech"; "Ezek"; "Ezechiel Propheta"; "Ezechiel"; "Ezekiel" ]); ("daniel", [ "Dan"; "Daniel Propheta"; "Daniel" ]); ("osee", [ "Osee"; "Osee Propheta" ]); ("joel", [ "Joel"; "Ioel"; "Ioel Propheta" ]); ("jonas", [ "Jonas"; "Ionae"; "Ionas Propheta" ]); ("malachi", [ "Mal"; "Malach"; "Malachias Propheta"; "Malachias"; "Malachi" ]); ("matthew", [ "Matt"; "Matt."; "Matth"; "Mat"; "Evangelium secundum Matthaeum"; "Matthew" ]); ("mark", [ "Mark"; "Marc"; "Evangelium secundum Marcum" ]); ("luke", [ "Luke"; "Luc"; "Evangelium secundum Lucam" ]); ("john", [ "John"; "Ioann"; "Evangelium secundum Ioannem" ]); ("acts", [ "Acts"; "Act"; "Actus Apostolorum"; "Acts of the Apostles"; "The Acts" ]); ("romans", [ "Rom"; "Epistola ad Romanos"; "Romans" ]); ("corinthians_1", [ "1 Cor"; "1 Cor."; "Epistola I ad Corinthios"; "1 Corinthians" ]); ("corinthians_2", [ "2 Cor"; "2 Cor."; "Epistola II ad Corinthios"; "2 Corinthians" ]); ("galatians", [ "Gal"; "Epistola ad Galatas"; "Galatians" ]); ("ephesians", [ "Eph"; "Eph."; "Ephes"; "Epistola ad Ephesios"; "Ephesians" ]); ("philippians", [ "Phil"; "Epistola ad Philippenses"; "Philippians" ]); ("colossians", [ "Col"; "Col."; "Epistola ad Colossenses"; "Colossians" ]); ("thessalonians_1", [ "1 Thess"; "1 Thess."; "Epistola I ad Thessalonicenses"; "1 Thessalonians" ]); ("thessalonians_2", [ "2 Thess"; "Epistola II ad Thessalonicenses"; "2 Thessalonians" ]); ("timothy_1", [ "1 Tim."; "1 Tim"; "Epistola I ad Timotheum"; "1 Timothy" ]); ("timothy_2", [ "2 Tim"; "2 Tim."; "Epistola II ad Timotheum"; "2 Timothy" ]); ("titus", [ "Titus"; "Tit"; "Epistola ad Titum" ]); ("hebrews", [ "Heb"; "Hebr"; "Epistola ad Hebraeos"; "Hebrews" ]); ("james", [ "Jas"; "James"; "Iac"; "Epistola beati Iacobi Apostoli" ]); ("peter_1", [ "1 Pet"; "1 Pet."; "1 Petri"; "Epistola I beati Petri Apostoli"; "1 Peter" ]); ("peter_2", [ "2 Pet."; "2 Petri"; "Epistola II beati Petri Apostoli"; "2 Pet"; "2 Peter" ]); ("john_1", [ "1 John"; "1 Ioann"; "Epistola beati Ioannis Apostoli" ]); (* "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"; "Liber Apocalypsis"; "Apocalypse" ]) ] (* Fix wave I2 (final-review.md, 2026-08-25-colitur-of-phases-3-5): books data/of/lectionary.sexp cites that no EF file ever needed, so the survey above never registered them -- 345 of 730 OF citation fields printed unconverted (35%) because [Book.of_token] had never heard of them, not because [Parse]'s grammar itself could not read the reference. English- canonical spellings only, verified directly against that one file (`grep`, not invented) -- the SAME "every spelling any of the files uses" discipline [table] above states, extended to a fourth file. Deliberately a SEPARATE table, not folded into [table] above, for one reason that matters: [table]'s ids are covered by [all] below, and book.mli's own contract for [all] is "every SHIPPED language file is asserted complete over" it (test_lang_coverage.ml's [test_every_book_ named] and its siblings). lang/la.ini's own header is equally strict -- "do not invent a Latin title", every `.full`/`.abbr` row cited to an exact scan line -- and NONE of these 25 books is attested anywhere in the EF's own scans (the EF lectionary/propers/commons never read from Ruth, Job, Judges, Philemon, ... at all). Registering them in [table] would force a choice between fabricating 50 uncited Latin titles (the one thing this project refuses hardest) and quietly weakening book.mli's own stated promise for the ids that DO have one. Neither is right, so these ids are resolvable ([of_token]/[tokens]/[default_spelling] below all include them) but excluded from [all] -- a citation using one still PARSES and RENDERS (bin/main.ml's own [names_of] already falls back to {!default_spelling} on a lang-table miss, book.mli's own documented reason that function exists), it just renders in its own English spelling rather than a Latin one no primary source has ever supplied, the exact "degraded, not broken" contract {!default_spelling} documents. [samuel_1]/[samuel_2] in particular are NOT [kings_1]/[kings_2] below: those tradition targets already denote (via traditions.ini's own [modern] mapping, "kings_3 = kings_1") the SAME physical book as the Vulgate's own [kings_3]/[kings_4] ("3/4 Kings" = modern "1/2 Kings") -- a different pair of books from Samuel ("Liber Regum I/II" in the Vulgate's own naming, never cited by any shipped EF data and so never given an id before now). *) let of_lectionary_table = [ ("samuel_1", [ "1 Sam"; "1 Samuel" ]); ("samuel_2", [ "2 Sam"; "2 Samuel" ]); ("joshua", [ "Josh"; "Joshua" ]); ("judges", [ "Judg"; "Judges" ]); ("ruth", [ "Ruth" ]); ("deuteronomy", [ "Deut"; "Deuteronomy" ]); ("chronicles_1", [ "1 Chron"; "1 Chronicles" ]); ("chronicles_2", [ "2 Chron"; "2 Chronicles" ]); ("ezra", [ "Ezra" ]); ("job", [ "Job" ]); ("ecclesiastes", [ "Eccl"; "Ecclesiastes" ]); ("baruch", [ "Bar"; "Baruch" ]); ("lamentations", [ "Lam"; "Lamentations" ]); ("amos", [ "Amos" ]); ("micah", [ "Mic"; "Micah" ]); ("nahum", [ "Nah"; "Nahum" ]); ("habakkuk", [ "Hab"; "Habakkuk" ]); ("zephaniah", [ "Zeph"; "Zephaniah" ]); ("haggai", [ "Hag"; "Haggai" ]); ("zechariah", [ "Zech"; "Zechariah" ]); ("maccabees_1", [ "1 Macc"; "1 Maccabees" ]); ("maccabees_2", [ "2 Macc"; "2 Maccabees" ]); ("philemon", [ "Phlm"; "Philemon" ]); ("jude", [ "Jude" ]); (* [john_2]/[john_3], siblings of [john_1] above -- three separate Johannine epistles, three separate ids, the same one-id-per-physical- book rule every other entry in this file follows. *) ("john_2", [ "2 John" ]); ("john_3", [ "3 John" ]) ] (* 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. *) (* The modern-numbering targets. They have table rows so that colitur's OWN rendered output re-parses: with --sigla-tradition modern the book prints as "1 Reg" or "1 Kings", and a user copying that back into an overlay must get it read correctly. The shipped data never cites them directly, which is why they are listed apart. *) let tradition_target_table = [ ("kings_1", [ "1 Reg"; "Liber Regum I"; "1 Kgs"; "1 Kings" ]); ("kings_2", [ "2 Reg"; "Liber Regum II"; "2 Kgs"; "2 Kings" ]); ("nehemiah", [ "Neh"; "Liber Nehemiae"; "Nehemiah" ]); ("sirach", [ "Liber Ecclesiastici"; "Sirach" ]); ("hosea", [ "Os"; "Hos"; "Hosea" ]); ("jonah", [ "Ion"; "Jon"; "Jonah" ]); ("revelation", [ "Apocalypsis"; "Revelation" ]); ] let tradition_targets = List.map fst tradition_target_table (* Deliberately NOT [of_lectionary_table] -- see that table's own header for why: those ids have no la.ini-verified name, so they stay out of the set {!Colitur_naming.Lang}-completeness checks (test_lang_coverage.ml) run against, without weakening what those checks still guarantee for every id here. *) let all = List.map fst table @ tradition_targets let tokens = List.concat_map (fun (id, sp) -> List.map (fun s -> (s, id)) sp) (table @ tradition_target_table @ of_lectionary_table) let default_spelling id = match List.assoc_opt id (table @ tradition_target_table @ of_lectionary_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 (* Fix wave I2: books with exactly one chapter are conventionally cited by VERSE ALONE, no chapter number at all ("Jude 17, 20b-25", "Phlm 7-22") -- found while adding [of_lectionary_table] above: [jude]'s own citation "Jude 17,20b-25" used to fail as "unknown book" (safe, if unconverted); registering [jude] made it PARSE, but WRONGLY -- [Parse]'s existing "a leading comma-piece that looks like a number introduces a chapter" rule (parse.ml's own [parse_part], written for real multi-chapter data like "15, 1-46") misread "17" as chapter 17, which does not exist in a 25-verse, one-chapter book. A parse that SUCCEEDS with the wrong structure is worse than one that fails: nothing downstream would ever suspect it. [Parse.parse] consults this to skip that rule entirely for these four ids and default to chapter 1. *) let single_chapter_ids = [ "jude"; "philemon"; "john_2"; "john_3" ] let is_single_chapter id = List.mem id single_chapter_ids