aboutsummaryrefslogtreecommitdiff
path: root/lib/citation/render.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:53:49 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:53:58 +0200
commit90584d87e763789808329847c46dbaa8e22dca47 (patch)
tree6b5e7c29adc0d1cf5fdfb71cfb8f097867bf04b5 /lib/citation/render.ml
parent9ebb06983b7a26db5564302253f2551dfbcf834e (diff)
downloadcolitur-90584d87e763789808329847c46dbaa8e22dca47.tar.gz
colitur-90584d87e763789808329847c46dbaa8e22dca47.zip
fix(citation): represent chapter-crossing verse ranges (W4)
Parse.verse_range's [last] endpoint gains an optional chapter (Parse.verse_end: { chapter : int option; verse : verse_num }), so a hyphen range whose two endpoints lie in different chapters ("1 John 1:5-2:2") can be represented at all. Rejected: a bare [int option] living alongside [last] as a second field on verse_range -- that would let 'a chapter with no verse' exist as a constructible value. parse_part now splits a part's leading "chapter:" at the FIRST colon only (not every colon), so the verses side can itself carry a second colon from a crossing range. parse_range detects a crossing by checking whether the range's right-hand side contains ':', and skips the same-chapter descending-range guard for that case (a later chapter is always "ahead", whatever its own verse numbers are). Handles the compound shape too -- a crossing range followed by further, same-chapter verse references in the same comma list ("Matthew 9:35-10:1,5a,6-8") -- since those trailing pieces parse as ordinary bare verses/ranges, unaffected by the preceding crossing. Render's one_range renders a crossing [last] through the same chapter_verse template one_part already uses for the part's own leading "chapter:verses", so a style that reconfigures the chapter/verse separator renders a crossing endpoint in that same convention rather than a hardcoded ':'. This closes the W4 known-wrong: 41 (now 49, after an intervening Second-reading extraction) of the OF lectionary's citations printed unconverted, every one this exact shape. test_citation_coverage_of.ml's pinned residual is now empty and asserted exactly, over the full 1725-field data/of/lectionary.sexp population, including the round-trip check (parse -> render -> parse structural equality). test_citation.ml gains direct parse-suite cases for the basic crossing, the compound shape, a mid-list crossing, a crossing with a sub-verse letter, and a malformed-crossing rejection. data/of/lectionary.sexp is regenerated via its own generator (tools/bootstrap_lectionary_of.ml, whose own embedded header text is updated to match); only comment lines change, confirmed by diff -- no lectionary entry differs. test_lectionary_of.ml's whole-file SHA-256 pin is updated to match. EF is unaffected: data/ef/ is untouched since v1.0.0, and a direct byte comparison of `colitur day`/`colitur readings` for 2026, 1583 and 9999 against a git-worktree build of 1c0137d is identical on all six outputs.
Diffstat (limited to 'lib/citation/render.ml')
-rw-r--r--lib/citation/render.ml19
1 files changed, 18 insertions, 1 deletions
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