diff options
| -rw-r--r-- | bin/main.ml | 179 | ||||
| -rw-r--r-- | man/colitur.1 | 183 | ||||
| -rw-r--r-- | test/cli.t | 69 |
3 files changed, 428 insertions, 3 deletions
diff --git a/bin/main.ml b/bin/main.ml index ec7fd71..5e3d07d 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -426,6 +426,75 @@ let emit_report ~format ~overlays ~dtstamp ~from_y ~to_y = exit 2 done +(* Task 9: `colitur table` and `colitur render` -- compute a year and render it + through a user-supplied template, in ONE process. + + The design's own sketch was `compute | render` as a Unix pipe, with `render` + reading a serialised view back from stdin. That is deliberately NOT built: + honouring the pipe would need a JSON *parser*, purely to re-read the view + this same process just serialised -- a second hand-rolled component, and a + second place for the published contract to drift, for no benefit over + calling [View.of_days] directly. So `table --year Y --template F` computes + and renders in one process (the command that actually gets used), and + `render --template F --year Y` is the identical operation under the name + the design used, kept so that documented vocabulary still works. There is + no stdin-fed `render`; `colitur emit --format json | jq` still composes for + real pipe use, because JSON there is the OUTPUT, never something colitur + itself has to parse back in. *) + +let read_file path = + match open_in_bin path with + | exception Sys_error _ -> Error ("cannot read template " ^ path) + | ic -> + let n = in_channel_length ic in + let s = really_input_string ic n in + close_in ic; + Ok s + +let extension path = + match String.rindex_opt path '.' with + | Some i -> String.sub path i (String.length path - i) + | None -> "" + +(* An unknown extension with no [--flavour] is an ERROR, never a silent + fallback to [Escape.None_]: guessing the flavour wrong produces malformed + output (unescaped LaTeX/HTML metacharacters) that looks fine until it does + not -- the same "never silently substitute" discipline [data_dir]'s own + [COLITUR_DATA_DIR] handling documents above. *) +let table_report ~template ~flavour_opt ~overlays y = + let flavour = + match flavour_opt with + | Some name -> ( + match Colitur_render.Escape.of_string name with + | Some f -> f + | None -> + Printf.eprintf "colitur: unknown flavour %S (want latex, groff, html, xml, ics or none)\n" + name; + exit 2) + | None -> ( + match Colitur_render.Escape.of_extension (extension template) with + | Some f -> f + | None -> + Printf.eprintf + "colitur: cannot infer a flavour from %S; pass --flavour latex|groff|html|xml|ics|none\n" + (extension template); + exit 2) + in + match read_file template with + | Error msg -> + Printf.eprintf "colitur: %s\n" msg; + exit 2 + | Ok src -> ( + let days = resolved_year_days ~overlays y in + let v = Colitur_render.View.of_days ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in + match Colitur_render.Template.render_string ~flavour src v with + | Error e -> + (* The template is user input; a parse failure is reported with the + parser's OWN reason and exits 2, never an uncaught exception. *) + Printf.eprintf "colitur: template %s: %s\n" template e; + exit 2 + | Ok out -> print_string out) + (* Help and usage are deliberately DIFFERENT things, and the difference is the Unix convention rather than a preference: asking for help is a request that SUCCEEDED, so [--help] prints to stdout and exits 0 (it can be piped into a @@ -455,6 +524,11 @@ usage: colitur emit --format csv|json|sexp|xml|ics --from Y --to Y [--overlay FILE ...] [--dtstamp S] render a resolved year range through one of five emitters + colitur table --year Y --template FILE [--flavour X] [--overlay FILE ...] + colitur render --template FILE --year Y [--flavour X] [--overlay FILE ...] + compute year Y and render it through FILE, a logic-less + Mustache-family template; table and render are the same + operation, two names (see "rendering" below) colitur new-overlay print a starter overlay file to stdout colitur convert FILE.ini flat INI overlay -> S-expression, on stdout colitur check FILE ... load an overlay, say what it does, exit 2 if not @@ -517,6 +591,55 @@ overlays: (Nth_weekday (month M) (nth N) (weekday W)) with N negative to count from the end of the month. +rendering: + --template FILE (required on `table`/`render`) is a logic-less Mustache- + family template: {{placeholder}}, {{#section}}...{{/section}}, + {{^inverted}}...{{/inverted}}, {{!comment}} -- nothing else. It is + DATA, never a program: no partials, no lambdas, no expression + evaluation, no filesystem or process access, and no "raw" or + triple-brace form that could opt out of escaping. The value it + renders against is the same schema `emit` uses (season, week, + slug, rank, colour, subject, names, citations, commemorations), + reshaped into a booklet (`days`) and a month grid (`weeks`, with + padding cells for the leading/trailing blanks); see colitur(1) + for the full field list. + + --flavour X selects how interpolated VALUES are escaped (never the + template's own literal markup, which is the author's). One of: + + latex groff html xml ics none + + Inferred from --template's extension when --flavour is omitted: + + .tex -> latex + .ms .mom .me -> groff + .html .htm -> html + .xml -> xml + .ics -> ics + .md .adoc .txt -> none (no metacharacters are escaped; + Markdown/AsciiDoc/plain text have no fixed + metacharacter set, so escaping them here + would produce worse output than leaving + them alone) + + An extension colitur does not recognise is a hard ERROR naming + the six flavours above, never a silent fallback to `none`: + guessing wrong produces output that looks fine until the + metacharacters it silently failed to escape show up. + + `table` and `render` are the SAME operation under two names. The design + this project followed originally sketched `compute | render` as a + Unix pipe, with `render` reading a serialised view back from + stdin. That is deliberately not built: honouring the pipe would + need a JSON *parser*, purely so this program could re-read a view + it had just serialised itself -- a second hand-rolled component, + and a second place for the published contract to drift, for no + benefit over calling the view builder directly in the same + process. There is therefore no stdin-fed `render`; `colitur emit + --format json | jq` still composes for real pipe use, because + that JSON is the OUTPUT, never something colitur itself parses + back in. + environment: COLITUR_DATA_DIR Read the calendar data from this directory instead of the @@ -564,12 +687,19 @@ let with_year ys f = [--format]/[--from]/[--to]/[--dtstamp] (Task 8, `emit`) are each single- valued, unlike [--overlay], so they are plain [string option] fields rather than accumulating lists. *) +(* [year]/[template]/[flavour] (Task 9, `table`/`render`) are each single- + valued, the same shape as [format]/[from_y]/[to_y]/[dtstamp] above -- + `table`/`render` take one year and one template file, never a range or a + repeatable list. *) type parsed_args = { overlays : string list; format : string option; from_y : string option; to_y : string option; dtstamp : string option; + year : string option; + template : string option; + flavour : string option; positional : string list; } @@ -586,6 +716,12 @@ let parse_args argv = | [ "--to" ] -> Error "--to needs a value" | "--dtstamp" :: v :: rest -> go { acc with dtstamp = Some v } rest | [ "--dtstamp" ] -> Error "--dtstamp needs a value" + | "--year" :: v :: rest -> go { acc with year = Some v } rest + | [ "--year" ] -> Error "--year needs a value" + | "--template" :: v :: rest -> go { acc with template = Some v } rest + | [ "--template" ] -> Error "--template needs a value" + | "--flavour" :: v :: rest -> go { acc with flavour = Some v } rest + | [ "--flavour" ] -> Error "--flavour needs a value" (* The recognised bare flags pass through as positional words for the dispatch below to match; anything else beginning with '-' is rejected rather than silently treated as a command or a year. *) @@ -596,7 +732,10 @@ let parse_args argv = Error (Printf.sprintf "unknown option %s" arg) | arg :: rest -> go { acc with positional = arg :: acc.positional } rest in - go { overlays = []; format = None; from_y = None; to_y = None; dtstamp = None; positional = [] } argv + go + { overlays = []; format = None; from_y = None; to_y = None; dtstamp = None; year = None; + template = None; flavour = None; positional = [] } + argv (* Sibling to [reject_overlays_for]: `emit`'s own four flags have no meaning on any other command (they take a single [<year>] positional, not a @@ -620,6 +759,18 @@ let reject_overlays_for cmd overlays = exit 2 end +(* Sibling to [reject_emit_flags_for]/[reject_overlays_for]: `table`/`render`'s + own three flags (Task 9) have no meaning on any other command, so accepting + and silently dropping them would be the same failure mode this project + already refuses everywhere else. *) +let reject_table_flags_for cmd ~year ~template ~flavour = + if year <> None || template <> None || flavour <> None then begin + Printf.eprintf + "colitur: --year/--template/--flavour have no effect on `%s`; refusing rather than ignoring them\n" + cmd; + exit 2 + end + (* `colitur check FILE...` -- load a user overlay, apply it to the real shipped calendar, and say what it did, without printing a year of output. @@ -784,46 +935,57 @@ let () = | Error msg -> Printf.eprintf "colitur: %s\n" msg; usage () - | Ok { overlays; format; from_y; to_y; dtstamp; positional } -> ( + | Ok { overlays; format; from_y; to_y; dtstamp; year; template; flavour; positional } -> ( let reject_emit = reject_emit_flags_for ~format ~from_y ~to_y ~dtstamp in + let reject_table = reject_table_flags_for ~year ~template ~flavour in match positional with | [ ("-h" | "--help" | "help") ] -> reject_overlays_for "--help" overlays; reject_emit "--help"; + reject_table "--help"; print_help () | [ ("-V" | "--version" | "version") ] -> reject_overlays_for "--version" overlays; reject_emit "--version"; + reject_table "--version"; print_endline version; exit 0 | [ "easter"; ys ] -> reject_overlays_for "easter" overlays; reject_emit "easter"; + reject_table "easter"; with_year ys easter_report | [ "temporal"; ys ] -> reject_overlays_for "temporal" overlays; reject_emit "temporal"; + reject_table "temporal"; with_year ys temporal_report | "check" :: (_ :: _ as files) -> reject_overlays_for "check" overlays; reject_emit "check"; + reject_table "check"; check_report files | [ "convert"; path ] -> reject_overlays_for "convert" overlays; reject_emit "convert"; + reject_table "convert"; convert_report path | [ "new-overlay" ] -> reject_overlays_for "new-overlay" overlays; reject_emit "new-overlay"; + reject_table "new-overlay"; print_string new_overlay_template; exit 0 | [ "day"; ys ] -> reject_emit "day"; + reject_table "day"; with_year ys (day_report ~overlays) | [ "readings"; ys ] -> reject_emit "readings"; + reject_table "readings"; with_year ys (readings_report ~overlays) | [ "emit" ] -> ( + reject_table "emit"; match format with | None -> Printf.eprintf "colitur: emit requires --format csv|json|sexp|xml|ics\n"; @@ -836,4 +998,17 @@ let () = | Some from_ys, Some to_ys -> with_year from_ys (fun from_y -> with_year to_ys (fun to_y -> emit_report ~format ~overlays ~dtstamp ~from_y ~to_y)))) + | [ ("table" | "render") as cmd ] -> ( + reject_emit cmd; + match template with + | None -> + Printf.eprintf "colitur: %s requires --year YEAR and --template FILE\n" cmd; + exit 2 + | Some template -> ( + match year with + | None -> + Printf.eprintf "colitur: %s requires --year YEAR and --template FILE\n" cmd; + exit 2 + | Some ys -> with_year ys (fun y -> table_report ~template ~flavour_opt:flavour ~overlays y) + )) | _ -> usage ()) diff --git a/man/colitur.1 b/man/colitur.1 index 1db7f9a..da08a36 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -21,6 +21,13 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, .RB [ \-\-dtstamp " STAMP" ] .br .B colitur +.BR table | render +.BI \-\-year " YEAR" +.BI \-\-template " FILE" +.RB [ \-\-flavour " FLAVOUR" ] +.RB [ \-\-overlay " FILE" " ...]" +.br +.B colitur .BR \-h | \-\-help .SH DESCRIPTION .B colitur @@ -80,6 +87,17 @@ See .B EMIT below. .TP +.BR table | render +Compute one civil year and render it through a user\-supplied template, in one +process. +.B table +and +.B render +are the same operation under two names \(em see +.B RENDERING +below for why there is no separate, stdin\-fed +.B render . +.TP .BI convert " FILE" .ini Convert a flat INI overlay to the S\-expression form, on standard output. The conversion verifies its own output before emitting it: the generated text is @@ -112,7 +130,7 @@ below. .TP .BI \-\-overlay " FILE" Apply a user calendar on top of the shipped one. Repeatable and ordered; -.BR day ", " readings " and " emit +.BR day ", " readings ", " emit ", " table " and " render only. See .B OVERLAYS below. @@ -145,6 +163,28 @@ is the emitted year. Never a clock read either way \(em see .B EMIT below. .TP +.BI \-\-year " YEAR" +.RB ( "colitur table" " and " "colitur render" " only)" +The civil year to compute, +.B 1583..9999 +as elsewhere. Required. +.TP +.BI \-\-template " FILE" +.RB ( "colitur table" " and " "colitur render" " only)" +The template file to render the year through. Required. See +.B RENDERING +below. +.TP +.BI \-\-flavour " FLAVOUR" +.RB ( "colitur table" " and " "colitur render" " only)" +One of +.BR latex ", " groff ", " html ", " xml ", " ics " or " none . +Overrides the flavour that would otherwise be inferred from +.BR \-\-template 's +own extension. See +.B RENDERING +below. +.TP .BR \-h ", " \-\-help Print a usage summary to standard output and exit 0. .TP @@ -299,6 +339,139 @@ applied on top of the shipped calendar, in order, before the range is rendered. See .B OVERLAYS below. +.SH RENDERING +.BI "colitur table " \-\-year " YEAR " \-\-template " FILE" +and +.BI "colitur render " \-\-template " FILE " \-\-year " YEAR" +are the +.I same +operation under two names: compute the resolved year, shape it into the +same view +.B emit +uses, and render it through +.I FILE +in one process. Both accept +.BR \-\-flavour " and " \-\-overlay +identically. +.SS Why there is no stdin\-fed render +The design this project followed originally sketched a Unix pipe, +.BR "compute | render" , +with +.B render +reading a serialised view back from standard input. That is deliberately +.I not +built. +Honouring the pipe would require a JSON +.I parser +inside +.B colitur +\(em a second hand\-rolled component, purely so this program could read back a +view it had just serialised itself, and a second place for the published +output schema to drift out of step with what the parser actually accepts. +That is a real cost for no benefit over calling the same view builder +directly in the same process, which is what +.B table +and +.B render +both do. +.PP +Unix composition is not abandoned, only narrowed to where it is cheap and +honest: +.B "colitur emit \-\-format json | jq" +still composes fine, because that JSON is the +.I output +of the pipeline, never something +.B colitur +itself has to parse back in. +.SS Templates +.I FILE +is a deliberately logic\-less, Mustache\-family template: it is +.I data, +never a program. The only constructs are +.BR {{placeholder}} , +.BR {{#section}}...{{/section}} , +.BR {{^inverted}}...{{/inverted}} +and +.BR {{!comment}} . +There are no partials, no lambdas, no expression evaluation, no arithmetic, +and no filesystem or process access from inside a template. There is +deliberately no "raw" or triple\-brace form either \(em a template cannot opt +out of its flavour's escaping. +.PP +The template renders against the same schema +.B emit +uses (season, week, slug, rank, colour, subject, names, citations, +commemorations), reshaped for two artefacts from one model: a flat booklet +(the +.B days +list, one entry per day of the year) and a month grid (the +.B weeks +list, with padding cells flagged for the leading and trailing blanks a grid +needs and a booklet does not). A key absent on a given day (an optional field +a rite does not always set) renders as the empty string rather than an error +\(em the one deliberate silence, so a template survives a day that does not +carry every optional field. +.SS Flavours +.BI \-\-flavour +controls how interpolated +.I values +are escaped for the target format. It never touches the template's own +literal markup, which is the author's and is trusted as\-is. One of: +.RS +.nf + +latex groff html xml ics none +.fi +.RE +.PP +When +.B \-\-flavour +is omitted it is inferred from +.BR \-\-template 's +own file extension: +.RS +.nf + +.I .tex -> latex +.I .ms .mom .me -> groff +.I .html .htm -> html +.I .xml -> xml +.I .ics -> ics +.I .md .adoc .txt -> none +.fi +.RE +.PP +.B none +escapes nothing: Markdown, AsciiDoc and plain text have no fixed +metacharacter set, so escaping them here would produce worse output than +leaving them alone. +.PP +An extension +.B colitur +does not recognise is a hard error naming the six flavours above; it is +.I never +a silent fallback to +.BR none . +Guessing the flavour wrong produces output that looks fine right up until +the metacharacters it silently failed to escape show up in a rendered +document. +.RS +.nf + +.B colitur table \-\-year 2027 \-\-template invite.wat +colitur: cannot infer a flavour from ".wat"; pass \-\-flavour latex|groff|html|xml|ics|none +.fi +.RE +.PP +A malformed template reports the parser's own reason and exits 2, never a +crash \(em a template is user input, exactly like an overlay file. +.RS +.nf + +.B colitur table \-\-year 2027 \-\-template bad.txt +colitur: template bad.txt: unclosed section {{#days}} +.fi +.RE .SH OVERLAYS .TP .BI \-\-overlay " FILE" @@ -484,6 +657,14 @@ Run against a checkout's data rather than the installed copy: .B COLITUR_DATA_DIR=~/git/projects/colitur/data/ef colitur day 2026 .fi .RE +.PP +Render a year through a template, flavour inferred from the extension: +.RS +.nf + +.B colitur table \-\-year 2026 \-\-template booklet.tex > booklet.tex.out +.fi +.RE .SH SOURCES The calendar is computed against the 1962 .I Missale Romanum @@ -484,3 +484,72 @@ day and readings are untouched: $ colitur readings 2027 | head -1 2027-01-01 ef-circumcision | Titus 2:11-15 | Luke 2:21 + +A minimal inline template renders -- table computes and renders in one +process (2 January 2027 is a Saturday, not a Sunday, so Holy Name Sunday +falls on the 3rd, not the 2nd, that year): + + $ printf '{{#days}}{{iso}} {{slug}}\n{{/days}}' > /tmp/t.txt + $ colitur table --year 2027 --template /tmp/t.txt | head -2 + 2027-01-01 ef-circumcision + 2027-01-02 ef-christmas-1-saturday + +render is the same operation under the name the design used: + + $ colitur render --template /tmp/t.txt --year 2027 | head -2 + 2027-01-01 ef-circumcision + 2027-01-02 ef-christmas-1-saturday + +Flavour is inferred from the extension and escapes data -- Sts. Peter & +Paul (29 June) and its vigil are the only two 2035 entries whose English +name needs LaTeX escaping: + + $ printf '{{#days}}{{name.en}}\n{{/days}}' > /tmp/t.tex + $ colitur table --year 2035 --template /tmp/t.tex | grep -c 'Peter \\& Paul' + 2 + +An unknown extension with no --flavour is an error, not a silent fallback: + + $ printf 'x' > /tmp/t.wat + $ colitur table --year 2027 --template /tmp/t.wat + colitur: cannot infer a flavour from ".wat"; pass --flavour latex|groff|html|xml|ics|none + [2] + + $ colitur table --year 2027 --template /tmp/t.wat --flavour none + x + +An unrecognised --flavour value is also an error naming the six valid ones: + + $ colitur table --year 2027 --template /tmp/t.txt --flavour bogus + colitur: unknown flavour "bogus" (want latex, groff, html, xml, ics or none) + [2] + +A malformed template is a clear error, not a crash: + + $ printf '{{#days}}oops' > /tmp/bad.txt + $ colitur table --year 2027 --template /tmp/bad.txt + colitur: template /tmp/bad.txt: unclosed section {{#days}} + [2] + +A missing template file is an error: + + $ colitur table --year 2027 --template /tmp/nope.txt + colitur: cannot read template /tmp/nope.txt + [2] + +table and render both require --year and --template: + + $ colitur table --year 2027 + colitur: table requires --year YEAR and --template FILE + [2] + + $ colitur render --template /tmp/t.txt + colitur: render requires --year YEAR and --template FILE + [2] + +table/render's own flags have no effect on the other commands, refused +rather than silently ignored: + + $ colitur emit --format csv --from 2027 --to 2027 --year 2028 + colitur: --year/--template/--flavour have no effect on `emit`; refusing rather than ignoring them + [2] |
