aboutsummaryrefslogtreecommitdiff
path: root/lib/citation/book.ml
blob: b43cdd81a29e1eee2f4b140fb4b0a29d815e73aa (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
(* 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