aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-14 17:05:51 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-14 17:05:51 +0200
commit6f5814ccdbc839b75c3e6de95a8ba9a755bc6ece (patch)
treefdb96f0b5da902e03d6aa1ae5e14d46755d9ea96 /tools
parent2c0187f42cc45864932aba2529f876be86ddba39 (diff)
downloadcolitur-6f5814ccdbc839b75c3e6de95a8ba9a755bc6ece.tar.gz
colitur-6f5814ccdbc839b75c3e6de95a8ba9a755bc6ece.zip
data(ef): bootstrap the temporal lectionary from lectio
119 entries, Epistle + Gospel only. The generator refuses a section carrying exactly one of the two: an Epistle without a Gospel is malformed and wants investigating, not shipping. SHA-256 of the source INI is in the provenance header, as with sanctoral.sexp.
Diffstat (limited to 'tools')
-rw-r--r--tools/bootstrap_lectionary.ml105
-rw-r--r--tools/dune8
2 files changed, 113 insertions, 0 deletions
diff --git a/tools/bootstrap_lectionary.ml b/tools/bootstrap_lectionary.ml
new file mode 100644
index 0000000..52c7e50
--- /dev/null
+++ b/tools/bootstrap_lectionary.ml
@@ -0,0 +1,105 @@
+(* Bootstraps data/ef/lectionary.sexp from lectio's tridentine-lectionary.ini.
+ Lives in tools/ and not the kernel for the same reason
+ bootstrap_sanctoral.ml does: reading someone else's INI needs a reader,
+ which does not belong in a pure kernel.
+
+ Only `first` and `gospel` are mapped -- the plan's Global Constraints fix
+ the scope at Epistle + Gospel, and lectio's data carries nothing else. *)
+
+open Colitur_kernel
+
+let default_source = "../lectio/internal/caldata/tridentine-lectionary.ini"
+let default_dest = "data/ef/lectionary.sexp"
+
+let die fmt =
+ Printf.ksprintf
+ (fun s -> prerr_endline ("bootstrap_lectionary: " ^ s); exit 1)
+ fmt
+
+type section = { name : string; fields : (string * string) list }
+
+let parse_ini path =
+ let ic = try open_in path with Sys_error e -> die "%s" e in
+ let sections = ref [] and cur = ref None in
+ let flush () =
+ match !cur with
+ | Some (n, fs) -> sections := { name = n; fields = List.rev fs } :: !sections
+ | None -> ()
+ in
+ (try
+ while true do
+ let line = String.trim (input_line ic) in
+ if line = "" || line.[0] = ';' || line.[0] = '#' then ()
+ else if line.[0] = '[' then begin
+ flush ();
+ cur := Some (String.sub line 1 (String.length line - 2), [])
+ end
+ else
+ match String.index_opt line '=' with
+ | None -> die "%s: cannot parse line %S" path line
+ | Some i ->
+ let k = String.trim (String.sub line 0 i) in
+ let v =
+ String.trim (String.sub line (i + 1) (String.length line - i - 1))
+ in
+ cur :=
+ (match !cur with
+ | Some (n, fs) -> Some (n, (k, v) :: fs)
+ | None -> die "%s: field %S before any section" path k)
+ done
+ with End_of_file -> ());
+ flush ();
+ close_in ic;
+ List.rev !sections
+
+let convert sec =
+ let slug =
+ match Slug.of_string sec.name with
+ | Ok s -> s
+ | Error e -> die "bad slug %S: %s" sec.name e
+ in
+ let cite part key =
+ match List.assoc_opt key sec.fields with
+ | None | Some "" -> None
+ | Some reference -> Some { Citation.part; reference }
+ in
+ let cs = List.filter_map Fun.id [ cite Citation.First "first"; cite Citation.Gospel "gospel" ] in
+ if cs = [] then die "%s: no first/gospel field" sec.name;
+ if List.length cs = 1 then
+ die "%s: has one reading, not two -- an Epistle without a Gospel (or the \
+ reverse) is malformed and must be investigated, not silently shipped"
+ sec.name;
+ (slug, cs)
+
+let sha256 path =
+ let ic = Unix.open_process_in (Printf.sprintf "sha256sum %s" (Filename.quote path)) in
+ let line = try input_line ic with End_of_file -> die "sha256sum failed" in
+ ignore (Unix.close_process_in ic);
+ List.hd (String.split_on_char ' ' line)
+
+let () =
+ let src = if Array.length Sys.argv > 1 then Sys.argv.(1) else default_source in
+ let dst = if Array.length Sys.argv > 2 then Sys.argv.(2) else default_dest in
+ let secs = parse_ini src in
+ let entries = List.map convert secs in
+ let lect =
+ match Lectionary.of_entries entries with
+ | Ok l -> l
+ | Error e -> die "%s" e
+ in
+ let oc = open_out dst in
+ Printf.fprintf oc
+ "; data/ef/lectionary.sexp -- EF (1962) temporal lectionary (Epistle +\n\
+ ; Gospel citations, never scripture text), bootstrapped from lectio.\n\
+ ; Generator: tools/bootstrap_lectionary.ml -- do not hand-edit; re-run the\n\
+ ; generator against a newer lectio and commit the diff instead.\n\
+ ;\n\
+ ; Source: %s\n\
+ ; SHA-256: %s\n\
+ ; %d entries. Regenerate with:\n\
+ ; eval $(opam env) && dune exec tools/bootstrap_lectionary.exe -- %s %s\n"
+ src (sha256 src) (List.length entries) src dst;
+ Sexplib.Sexp.output_hum oc (Lectionary.sexp_of_t lect);
+ output_char oc '\n';
+ close_out oc;
+ Printf.printf "bootstrap_lectionary: %d entries -> %s\n" (List.length entries) dst
diff --git a/tools/dune b/tools/dune
index d53cd01..fa6943d 100644
--- a/tools/dune
+++ b/tools/dune
@@ -8,3 +8,11 @@
(executable
(name bootstrap_sanctoral)
(libraries colitur_kernel rite_ef unix sexplib))
+
+; Converts lectio's tridentine-lectionary.ini into data/ef/lectionary.sexp.
+; Run via `dune exec tools/bootstrap_lectionary.exe -- <source.ini> <dest.sexp>`.
+; Same `unix` usage as bootstrap_sanctoral above: shells out to `sha256sum`
+; for the provenance header only, never a new opam dependency.
+(executable
+ (name bootstrap_lectionary)
+ (libraries colitur_kernel unix sexplib))