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
|
(* SPDX-License-Identifier: AGPL-3.0-or-later *)
(** A citation, parsed. Never raises; an unrecognised string is an [Error]
naming what could not be read, never a silent pass-through.
The shape is a book and a LIST of chapter-parts, not one chapter and one
verse range, because the shipped data really does cite across chapters
([John 18:1-40; 19:1-42]) and really does list disjoint verse ranges
within a chapter ([Dan 13:1-9, 15-17, 19-30, 33-62]). *)
(** A verse number, plus an optional trailing sub-verse letter run ([""] for
the overwhelming majority -- see parse.ml's own citation): ["11a"] is
[{ n = 11; suffix = "a" }], preserved through parsing and rendering
rather than dropped, so a reader never loses precision the source text
actually carried. *)
type verse_num = { n : int; suffix : string }
(** 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 }
val parse : string -> (t, string) result
|