summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/citation/book.ml5
-rw-r--r--lib/citation/book.mli19
-rw-r--r--test/test_citation.ml48
3 files changed, 72 insertions, 0 deletions
diff --git a/lib/citation/book.ml b/lib/citation/book.ml
index 545e324..2a2b9c0 100644
--- a/lib/citation/book.ml
+++ b/lib/citation/book.ml
@@ -78,6 +78,11 @@ 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
diff --git a/lib/citation/book.mli b/lib/citation/book.mli
index 8918bc3..0ca229c 100644
--- a/lib/citation/book.mli
+++ b/lib/citation/book.mli
@@ -36,6 +36,25 @@ 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
diff --git a/test/test_citation.ml b/test/test_citation.ml
index e234942..9405d39 100644
--- a/test/test_citation.ml
+++ b/test/test_citation.ml
@@ -31,6 +31,44 @@ let test_both_spellings_are_one_book () =
same "Ezech" "Ezek";
same "Jas" "James"
+(* The ordinal spellings resolve in the token table. NOTE: this does NOT
+ exercise [Parse.split_book]'s leading-digit scan -- of_token is a plain
+ table lookup. The parse-layer cases in [parse_suite] cover that; both are
+ needed, and confusing the two is how the gap survived review once already. *)
+let test_ordinal_books_beyond_one () =
+ let id t =
+ match B.of_token t with
+ | Some x -> B.to_string x
+ | None -> Alcotest.failf "%s did not resolve" t
+ in
+ Alcotest.(check s) "3 Kings" "kings_3" (id "3 Kings");
+ Alcotest.(check s) "3 Kgs." "kings_3" (id "3 Kgs.");
+ Alcotest.(check s) "4 Kings" "kings_4" (id "4 Kings")
+
+(* The fallback display name: what a book renders as when a language file has
+ no [bible] entry for it. Must be the data's own spelling, never the id --
+ and must itself re-parse, or Render/Parse round-tripping breaks. *)
+let test_default_spelling () =
+ let sp t =
+ match B.of_token t with
+ | Some x -> B.default_spelling x
+ | None -> Alcotest.failf "%s did not resolve" t
+ in
+ Alcotest.(check s) "luke" "Luke" (sp "Luke");
+ Alcotest.(check s) "kings_3 uses first spelling" "3 Kings" (sp "3 Kgs.");
+ let cited = List.map snd B.tokens in
+ List.iter
+ (fun id ->
+ if List.mem id cited then begin
+ let d = B.default_spelling id in
+ match B.of_token d with
+ | Some back when B.to_string back = B.to_string id -> ()
+ | _ ->
+ Alcotest.failf "default_spelling %s = %S does not resolve back"
+ (B.to_string id) d
+ end)
+ B.all
+
let test_unknown_token_is_none () =
Alcotest.(check bool) "not a book" true (B.of_token "Nonesuch" = None)
@@ -139,6 +177,8 @@ let test_every_data_file_token_resolves () =
let suite =
( "book",
[ Alcotest.test_case "both spellings one book" `Quick test_both_spellings_are_one_book;
+ Alcotest.test_case "ordinal books 3 and 4" `Quick test_ordinal_books_beyond_one;
+ Alcotest.test_case "default spelling round-trips" `Quick test_default_spelling;
Alcotest.test_case "unknown token" `Quick test_unknown_token_is_none;
Alcotest.test_case "vulgate identity" `Quick test_vulgate_is_identity;
Alcotest.test_case "modern renumbers" `Quick test_modern_renumbers;
@@ -198,6 +238,14 @@ let parse_suite =
("four parts", `Quick, parses "Eccli 24:5; 14:7; 14:9-11; 24:30-31"
"ecclesiasticus|24:5|14:7|14:9-11|24:30-31");
("modern name, vulgate id", `Quick, parses "Rev 12:1" "apocalypse|12:1");
+ (* Ordinals 3 and 4 must survive [split_book]'s leading-digit scan.
+ Narrowing its '1'..'4' range to '1'..'3' passes every OTHER case in
+ this suite silently, while seven real citations depend on it. A
+ Book.of_token test does NOT cover this -- that is a table lookup and
+ never reaches split_book. *)
+ ("ordinal 3 parses", `Quick, parses "3 Kings 17:8-16" "kings_3|17:8-16");
+ ("ordinal 3 dotted", `Quick, parses "3 Kgs. 19:3-8" "kings_3|19:3-8");
+ ("ordinal 4 parses", `Quick, parses "4 Kings 5:1-15" "kings_4|5:1-15");
("unknown book", `Quick, test_rejects_unknown_book);
("garbage", `Quick, test_rejects_garbage) ]