aboutsummaryrefslogtreecommitdiff
path: root/lib/citation
diff options
context:
space:
mode:
Diffstat (limited to 'lib/citation')
-rw-r--r--lib/citation/parse.ml76
-rw-r--r--lib/citation/parse.mli14
-rw-r--r--lib/citation/render.ml19
3 files changed, 92 insertions, 17 deletions
diff --git a/lib/citation/parse.ml b/lib/citation/parse.ml
index 2c73600..811019e 100644
--- a/lib/citation/parse.ml
+++ b/lib/citation/parse.ml
@@ -13,12 +13,30 @@
it would silently lose real precision a reader can see in the source
text, trading "wrong format, complete" for "right format, incomplete". *)
type verse_num = { n : int; suffix : string }
-type verse_range = { first : verse_num; last : verse_num option }
+
+(* See parse.mli's own citation for [verse_end]'s rationale. *)
+type verse_end = { chapter : int option; verse : verse_num }
+type verse_range = { first : verse_num; last : verse_end 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
+(* Split at the FIRST ':' only, unlike [split_on ':'] which splits at every
+ one. [parse_part] needs this: a "chapter:verses" part whose verses
+ themselves contain a chapter-crossing hyphen range ("1:5-2:2") carries a
+ SECOND colon that belongs to the verses side, not to a second chapter
+ marker -- splitting on every ':' would see three pieces there and fail
+ to parse at all (the exact shape [parse_range]'s own citation above
+ restores). *)
+let split_first_colon s =
+ match String.index_opt s ':' with
+ | None -> None
+ | Some i ->
+ Some
+ ( String.trim (String.sub s 0 i),
+ String.trim (String.sub s (i + 1) (String.length s - i - 1)) )
+
(* 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. *)
@@ -109,18 +127,44 @@ let num_opt s : verse_num option =
| None -> None
| Some (digits, suffix) -> ( match int_opt digits with Some n -> Some { n; suffix } | None -> None)
-(* "20-32" -> {first=20; last=Some 32}; "21" -> {first=21; last=None} *)
+(* "20-32" -> {first=20; last=Some{chapter=None; verse=32}};
+ "21" -> {first=21; last=None};
+ "35-10:1" -> {first=35; last=Some{chapter=Some 10; verse=1}} -- the
+ RIGHT side of a hyphen range may itself carry an explicit "chapter:verse"
+ (a range whose hyphen crosses a chapter boundary, [verse_end]'s own
+ citation in the .mli). The LEFT side never does: it is always the
+ chapter already in effect at that point in the part (parse_part's own
+ [c], or the chapter a PRECEDING range in the same part most recently
+ crossed into -- an ambiguity the source text itself never resolves and
+ this function does not try to, since nothing downstream needs the first
+ side's chapter: {!Colitur_citation.Render} only ever prints a part's
+ OWN [chapter] once, at the front, from [parse_part]'s [c]). *)
let parse_range s =
match split_on '-' s with
| [ a ] -> ( match num_opt a with Some f -> Some { first = f; last = None } | None -> None)
| [ a; b ] -> (
- match (num_opt a, num_opt b) with
- (* A descending range ("1:20-10") is always a transcription error;
- accepting it would render back out as a citation nobody can follow.
- Compared on the NUMBER only -- "11a-11b" is a real, ascending
- sub-verse range even though nothing here orders letters. *)
- | Some f, Some l when l.n >= f.n -> Some { first = f; last = Some l }
- | _ -> None)
+ match num_opt a with
+ | None -> None
+ | Some f ->
+ if String.contains b ':' then
+ (* The right side names its own chapter explicitly: no verse-
+ number ordering constraint applies (a later chapter is
+ always "ahead", whatever its own verse numbers are). *)
+ match split_on ':' b with
+ | [ cs; vs ] -> (
+ match (int_opt cs, num_opt vs) with
+ | Some c, Some v -> Some { first = f; last = Some { chapter = Some c; verse = v } }
+ | _ -> None)
+ | _ -> None
+ else
+ match num_opt b with
+ (* A descending range ("1:20-10") is always a transcription
+ error; accepting it would render back out as a citation
+ nobody can follow. Compared on the NUMBER only -- "11a-11b"
+ is a real, ascending sub-verse range even though nothing
+ here orders letters. *)
+ | Some l when l.n >= f.n -> Some { first = f; last = Some { chapter = None; verse = l } }
+ | _ -> None)
| _ -> None
let parse_ranges s =
@@ -140,13 +184,16 @@ let parse_ranges s =
always a VERSE, never a chapter introduction -- see book.ml's own
citation for the real, wrongly-parsed example this was found on. *)
let parse_part ~single_chapter ~inherited s =
- match split_on ':' s with
- | [ c; v ] -> (
- (* explicit "chapter:verses" *)
+ match split_first_colon s with
+ | Some (c, v) -> (
+ (* explicit "chapter:verses" -- [v] may itself carry further colons,
+ from a chapter-crossing hyphen range inside it; [parse_ranges] /
+ [parse_range] read those, this function does not need to. *)
match (int_opt c, parse_ranges v) with
| Some ch, Some vs -> Some { chapter = ch; verses = vs }
| _ -> None)
- | [ only ] ->
+ | None -> (
+ let only = s in
if single_chapter then
match parse_ranges only with
| Some vs -> Some { chapter = Option.value inherited ~default:1; verses = vs }
@@ -171,8 +218,7 @@ let parse_part ~single_chapter ~inherited s =
match parse_ranges only with
| Some vs -> Some { chapter = ch; verses = vs }
| None -> None)
- | _, None -> None)
- | _ -> None
+ | _, None -> None))
let parse s =
let s = String.trim s in
diff --git a/lib/citation/parse.mli b/lib/citation/parse.mli
index ea38d24..bc08728 100644
--- a/lib/citation/parse.mli
+++ b/lib/citation/parse.mli
@@ -15,7 +15,19 @@
actually carried. *)
type verse_num = { n : int; suffix : string }
-type verse_range = { first : verse_num; last : verse_num option }
+(** The end of a verse range, when it needs to say more than a bare verse
+ number. [chapter = None] is the overwhelming common case (the range
+ ends in the same chapter its [part] already names); [chapter = Some c]
+ is a range whose hyphen crosses into a LATER chapter -- the shipped OF
+ lectionary really does cite this shape ([1 John 1:5-2:2]: the range
+ starts in chapter 1, the part's own [chapter], and ends at 2:2). A
+ dedicated record, rather than a bare [int option] living alongside
+ [verse_range.last] as a second field, so "a chapter with no verse" is
+ not a state this type can even represent -- see parse.ml's own
+ [parse_range] for how a hyphen range decides which shape it is. *)
+type verse_end = { chapter : int option; verse : verse_num }
+
+type verse_range = { first : verse_num; last : verse_end option }
type part = { chapter : int; verses : verse_range list }
type t = { book : Book.id; parts : part list }
diff --git a/lib/citation/render.ml b/lib/citation/render.ml
index 2b40ab3..d85b050 100644
--- a/lib/citation/render.ml
+++ b/lib/citation/render.ml
@@ -81,10 +81,27 @@ let subst tmpl pairs =
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 -> subst st.range [ ("first", show_num r.Parse.first); ("last", show_num l) ]
+ | 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