From 6f5814ccdbc839b75c3e6de95a8ba9a755bc6ece Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 14 Aug 2026 17:05:51 +0200 Subject: 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. --- tools/bootstrap_lectionary.ml | 105 ++++++++++++++++++++++++++++++++++++++++++ tools/dune | 8 ++++ 2 files changed, 113 insertions(+) create mode 100644 tools/bootstrap_lectionary.ml (limited to 'tools') 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 -- `. +; 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)) -- cgit v1.3