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
|
(* SPDX-License-Identifier: AGPL-3.0-or-later *)
type verse_range = { first : int; last : int 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
(* 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. *)
let split_book s =
let n = String.length s in
let i = ref 0 in
(* optional leading ordinal digit *)
if !i < n && s.[!i] >= '1' && s.[!i] <= '4' then begin
incr i;
while !i < n && s.[!i] = ' ' do incr i done
end;
(* letters and dots *)
while !i < n && (s.[!i] = '.' || (s.[!i] >= 'A' && s.[!i] <= 'z')) do incr i done;
if !i = 0 then None
else
let book = String.trim (String.sub s 0 !i) in
let tail = String.trim (String.sub s !i (n - !i)) in
if book = "" || tail = "" then None else Some (book, tail)
let int_opt s = int_of_string_opt (String.trim s)
(* "20-32" -> {first=20; last=Some 32}; "21" -> {first=21; last=None} *)
let parse_range s =
match split_on '-' s with
| [ a ] -> ( match int_opt a with Some f -> Some { first = f; last = None } | None -> None)
| [ a; b ] -> (
match (int_opt a, int_opt b) with
| Some f, Some l -> Some { first = f; last = Some l }
| _ -> None)
| _ -> None
let parse_ranges s =
let pieces = split_on ',' s in
List.fold_right
(fun p acc ->
match (parse_range p, acc) with
| Some r, Some rest -> Some (r :: rest)
| _ -> None)
pieces (Some [])
(* One ";"-separated part. [inherited] is the chapter of the previous part,
used when this one names none (rule 1 in the grammar table). *)
let parse_part ~inherited s =
match split_on ':' s with
| [ c; v ] -> (
(* explicit "chapter:verses" *)
match (int_opt c, parse_ranges v) with
| Some ch, Some vs -> Some { chapter = ch; verses = vs }
| _ -> None)
| [ only ] -> (
(* Either "chapter, verses" (rule 3) or bare verses inheriting a
chapter. Distinguish on whether the FIRST comma-piece is a lone
number followed by more pieces -- a leading number followed by
at least one further piece is a chapter introduction ("15, 1-46");
a lone piece on its own, or a part with no more pieces to follow,
can only be verses inheriting the previous chapter. *)
let pieces = split_on ',' only in
match (pieces, inherited) with
| first :: (_ :: _ as rest), _ when int_opt first <> None && String.contains only ',' -> (
(* "15, 1-46" -> chapter 15. This never fires for a part that
already named its chapter via ":" -- those match the [c; v]
branch above and never reach here. *)
match (int_opt first, parse_ranges (String.concat "," rest)) with
| Some ch, Some vs -> Some { chapter = ch; verses = vs }
| _ -> None)
| _, Some ch -> (
match parse_ranges only with
| Some vs -> Some { chapter = ch; verses = vs }
| None -> None)
| _, None -> None)
| _ -> None
let parse s =
let s = String.trim s in
(* A trailing period is decoration, not data: 22 citations carry one. *)
let s =
let n = String.length s in
if n > 0 && s.[n - 1] = '.' then String.sub s 0 (n - 1) else s
in
match split_book s with
| None -> Error "no book"
| Some (btok, tail) -> (
match Book.of_token btok with
| None -> Error ("unknown book: " ^ btok)
| Some book ->
(* A trailing ";" leaves an empty piece: drop it rather than
failing. Only if nothing remains is it an error. *)
let pieces = List.filter (fun p -> p <> "") (split_on ';' tail) in
let rec go inherited = function
| [] -> Ok []
| p :: rest -> (
match parse_part ~inherited p with
| None -> Error ("cannot read reference: " ^ p)
| Some part -> (
match go (Some part.chapter) rest with
| Error e -> Error e
| Ok more -> Ok (part :: more)))
in
(match go None pieces with
| Error e -> Error e
| Ok [] -> Error "empty reference"
| Ok parts -> Ok { book; parts }))
|