aboutsummaryrefslogtreecommitdiff
path: root/man/colitur.1
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-19 11:48:30 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-19 11:48:30 +0200
commit6762ce46af3cb12bc6ae37cda762c5d95add7903 (patch)
treebc1d8c86050d0149ff961a9a4ff838f9c474ac2a /man/colitur.1
parent897c274fd28402159ca6d45eedc1257b1ce98696 (diff)
parent390bc6ac5196a946c473d0dbe7760fa41837c428 (diff)
downloadcolitur-6762ce46af3cb12bc6ae37cda762c5d95add7903.tar.gz
colitur-6762ce46af3cb12bc6ae37cda762c5d95add7903.zip
feat: output, rendering and publishing
Gives colitur a publishable exit. Until now its only output was terminal rows; it can now print an ordo booklet and a wall calendar, publish an iCalendar feed people subscribe to, and serve a static JSON/XML API. lib/render escaping (six flavours + RFC 5545 folding), a deliberately logic-less template engine, the view model, and five emitters (CSV, JSON, XML, iCalendar, S-expression) CLI emit, table, render, publish -- all accepting --overlay templates ordo booklet in six flavours, wall grid in three schema day-v1.json and colitur-v1.xsd, the published contract man colitur-templates.5, plus colitur.1 updates The view model is why the engine can stay logic-less: a month grid needs leading blank cells, week bucketing and an in-month test, and a logic-less template can compute none of it. Shaping the data in OCaml keeps the engine safe for untrusted templates and makes the grid trivial. Formats split by whether correctness is mechanical. Presentation goes through templates; iCalendar and XML get dedicated emitters, because folding, exclusive DTEND, stable UIDs and schema fidelity are rules a template cannot enforce and each fails silently in a subscriber's client rather than loudly at generation. publish is deterministic and non-destructive: two runs produce a byte-identical tree, and --prune removes only files a previous run created, refusing any manifest entry that escapes the output directory. No new dependencies. The kernel and rite modules are untouched, and colitur day and colitur readings remain byte-identical.
Diffstat (limited to 'man/colitur.1')
-rw-r--r--man/colitur.1501
1 files changed, 493 insertions, 8 deletions
diff --git a/man/colitur.1 b/man/colitur.1
index 58e46ff..005cc0a 100644
--- a/man/colitur.1
+++ b/man/colitur.1
@@ -13,6 +13,30 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite,
.RI [ ... ]
.br
.B colitur
+.B emit
+.BI \-\-format " FMT"
+.BI \-\-from " YEAR"
+.BI \-\-to " YEAR"
+.RB [ \-\-overlay " FILE" " ...]"
+.RB [ \-\-dtstamp " STAMP" ]
+.br
+.B colitur
+.BR table | render
+.BI \-\-year " YEAR"
+.BI \-\-template " FILE"
+.RB [ \-\-flavour " FLAVOUR" ]
+.RB [ \-\-overlay " FILE" " ...]"
+.br
+.B colitur
+.B publish
+.BI \-\-from " YEAR"
+.BI \-\-to " YEAR"
+.BI \-\-out " DIR"
+.RB [ \-\-overlay " FILE" " ...]"
+.RB [ \-\-prune ]
+.RB [ \-\-dtstamp " STAMP" ]
+.br
+.B colitur
.BR \-h | \-\-help
.SH DESCRIPTION
.B colitur
@@ -65,6 +89,32 @@ occurrence, commemoration and transfer.
.BI readings " YEAR"
The Mass reading citations, one line per day.
.TP
+.B emit
+Render a civil\-year range through one of five emitters \(em
+.BR csv ", " json ", " sexp ", " xml " or " ics .
+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
+.B publish
+Write the static tree that
+.I is
+this program's API: a civil\-year range rendered once, as files, so any web
+server or git repository can serve it and nothing runs at request time. See
+.B PUBLISH
+below.
+.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
@@ -97,13 +147,81 @@ below.
.TP
.BI \-\-overlay " FILE"
Apply a user calendar on top of the shipped one. Repeatable and ordered;
-.B day
-and
-.B readings
+.BR day ", " readings ", " emit ", " table ", " render " and " publish
only. See
.B OVERLAYS
below.
.TP
+.BI \-\-format " FMT"
+.RB ( "colitur emit" " only)"
+One of
+.BR csv ", " json ", " sexp ", " xml " or " ics .
+Required. See
+.B EMIT
+below.
+.TP
+.BI \-\-from " YEAR" ", " \-\-to " YEAR"
+.RB ( "colitur emit" " and " "colitur publish" " only)"
+The inclusive civil\-year range to render, each
+.B 1583..9999
+as elsewhere.
+.I FROM
+must not be after
+.IR TO .
+Both required.
+.TP
+.BI \-\-dtstamp " STAMP"
+.RB ( "colitur emit \-\-format ics" " and " "colitur publish" " only)"
+Fix the feed's own DTSTAMP instead of the default
+.IR YYYY0101T000000Z ,
+where
+.I YYYY
+is the emitted year. Never a clock read either way \(em see
+.B EMIT
+below.
+.TP
+.BI \-\-out " DIR"
+.RB ( "colitur publish" " only)"
+The directory to write the static tree into. Created if it does not exist.
+Required. See
+.B PUBLISH
+below.
+.TP
+.B \-\-prune
+.RB ( "colitur publish" " only)"
+Remove files a previous
+.B publish
+run into the same
+.B \-\-out
+wrote that this run did not rewrite. Never removes a file that is not
+recorded in
+.IR out /.colitur\-manifest ,
+regardless of this flag. See
+.B PUBLISH
+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
@@ -162,6 +280,349 @@ Both reports are one line per day and ordered by date, so they compose with
and
.BR join (1)
in the ordinary way.
+.SH EMIT
+.BI "colitur emit " \-\-format " FMT " \-\-from " YEAR " \-\-to " YEAR"
+renders the same resolved day \(em season, week, slug, rank, colour,
+subject, Latin and English names, citations, commemorations \(em through one
+of five emitters, for every day in the inclusive civil\-year range
+.IR FROM .. TO .
+Every emitter consumes one shared view of the data, so all five describe
+exactly the same fields.
+.TP
+.B csv
+RFC 4180. One header row for the whole run, not one per year, so a
+multi\-year range still has exactly one header and
+.BR wc (1)
+or
+.B "awk 'NR>1'"
+behave as expected.
+.RS
+.nf
+
+.B colitur emit \-\-format csv \-\-from 2026 \-\-to 2026 | head \-2
+date,rite,season,week,slug,rank,colour,subject,name_la,name_en,first,gospel,comms
+2026\-01\-01,ef,christmastide,,ef\-circumcision,class\-1,white,temporal,,,Titus 2:11\-15,Luke 2:21,
+.fi
+.RE
+.TP
+.B json
+One JSON object per requested year, concatenated. Shape pinned by
+.IR schema/day\-v1.json .
+.RS
+.nf
+
+.B colitur emit \-\-format json \-\-from 2026 \-\-to 2026 | head \-c 40
+{"rite":"ef","year":"2026","months":[{...
+.fi
+.RE
+.TP
+.B sexp
+One S\-expression per day, one per line \(em the same
+.I Liturgical_day.t
+shape used internally, printed with
+.IR sexplib "'s " to_string_hum .
+.TP
+.B xml
+Element\-per\-field, one
+.I <calendar>
+document per requested year, concatenated. Attributes carry identity only
+(rite, year, date); everything else is an element. Shape pinned by
+.IR schema/colitur\-v1.xsd ,
+checked by
+.B make check\-schema
+when
+.BR xmllint (1)
+is installed.
+.TP
+.B ics
+RFC 5545. One
+.I VCALENDAR
+per requested year, concatenated, one all\-day
+.I VEVENT
+per day. Lines are folded at 75 octets and end
+.RI ( CRLF ),
+matching the protocol exactly \(em
+.RB \(lq " cat \-A " \(rq
+on the output shows
+.B ^M$
+at each line end.
+.RS
+.nf
+
+.B colitur emit \-\-format ics \-\-from 2026 \-\-to 2026 | head \-1
+BEGIN:VCALENDAR
+.fi
+.RE
+.PP
+.B \-\-dtstamp
+fixes the feed's own
+.I DTSTAMP
+field, which RFC 5545 requires on every event. Without it the value defaults
+to
+.I YYYY0101T000000Z
+for the emitted year \(em a fixed value, not a clock read \(em so two
+.B emit \-\-format ics
+runs over identical data are byte\-identical, which matters for a
+reproducible build or a diffable published calendar file. Nothing in the
+.B emit
+path reads the wall clock, for any format.
+.PP
+.BR \-\-overlay
+is accepted exactly as on
+.B day
+and
+.BR readings :
+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.
+.PP
+See
+.BR colitur\-templates (5)
+for the full syntax, the escaping table per flavour, the complete
+view\-model field reference, and \(em before writing a template of any
+complexity \(em its
+.B SCOPE AND LOOKUP
+section: an inner key silently loses to an outer key of the same name (a bare
+.B {{name.la}}
+inside a day resolves to the enclosing MONTH's name, not the day's own),
+which has produced wrong output in this project's own templates.
+.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 PUBLISH
+.BI "colitur publish " \-\-from " YEAR " \-\-to " YEAR " \-\-out " DIR"
+writes the static tree that
+.I is
+this program's API: every file a civil\-year range can be asked for,
+computed once and written out, so any web server or git repository can
+serve the result as\-is and nothing runs at request time.
+.RS
+.nf
+
+ef/<year>.json one civil year, all days, whole\-year emitters
+ef/<year>.csv
+ef/<year>.xml
+ef/<year>.ics
+ef/<year>/<mm>/<dd>.json one file per day
+schema/day\-v1.json the published JSON contract
+index.html a generated index page, not a template
+\&.colitur\-manifest every path this run wrote, one per line
+.fi
+.RE
+.PP
+Every emitted file goes through the same emitters
+.B emit
+uses; a published
+.I .ics
+file for a given year is byte\-for\-byte what
+.B "colitur emit \-\-format ics"
+would print for that year, and
+.B \-\-dtstamp
+means exactly what it means there. The per\-day JSON files carry the same
+shape as the whole\-year one, scoped to a single day \(em
+.B "colitur table"
+and template authors needing one day's data can read either.
+.PP
+.B Deterministic.
+Publishing the same
+.B \-\-from / \-\-to
+range into an empty directory twice produces a byte\-identical tree. Nothing
+in the publish path reads the wall clock; the
+.I .ics
+files' own DTSTAMP defaults to a fixed value derived from the emitted year,
+exactly as it does under
+.B emit
+(see
+.B EMIT
+above), and
+.B \-\-dtstamp
+overrides it the same way. This is what makes publishing into a git
+repository safe:
+.B git status
+shows only genuine change, and you review an actual diff before pushing,
+never a rewrite of files that did not change.
+.PP
+.B Non\-destructive.
+.B publish
+writes only files it owns, and records the relative path of every one of
+them in
+.IR out /.colitur\-manifest
+(itself never subject to pruning). A file you put in the output directory
+yourself \(em by hand, or from some other tool \(em is never named in that
+manifest, so it is never touched,
+.I whether or not
+.B \-\-prune
+is given.
+.RS
+.nf
+
+.B "touch out/MY\-NOTES.txt"
+.B "colitur publish \-\-from 2027 \-\-to 2027 \-\-out out \-\-prune"
+.B "test \-f out/MY\-NOTES.txt && echo kept"
+kept
+.fi
+.RE
+.PP
+.B \-\-prune
+removes exactly the entries a
+.I previous
+publish into the same
+.B \-\-out
+wrote that this run did not rewrite \(em typically an earlier year's own
+per\-day files, when a later
+.B publish
+targets a different
+.B \-\-from / \-\-to
+range into the same directory. A directory a stale entry's removal leaves
+empty is removed too (so, for example,
+.I out/ef/2027/
+itself goes away once every file under it is gone), but nothing above
+.B \-\-out
+is ever touched, and
+.B \-\-out
+itself is never removed even when nothing is left in it. Without
+.BR \-\-prune ,
+old entries are left in place, and only the manifest is rewritten to
+describe the current run.
+.PP
+.BR \-\-overlay
+is accepted exactly as on
+.BR day ", " readings " and " emit :
+applied on top of the shipped calendar, in order, before each year in the
+range is rendered. See
+.B OVERLAYS
+below.
.SH OVERLAYS
.TP
.BI \-\-overlay " FILE"
@@ -242,11 +703,9 @@ shapes:
.RE
.PP
Accepted on
-.B day
-and
-.B readings
-only. The other commands read no sanctoral data at all, so the flag would have
-no effect there and is
+.BR day ", " readings ", " emit ", " table ", " render " and " publish .
+.BR easter " and " temporal
+read no sanctoral data at all, so the flag would have no effect there and is
.I refused
rather than silently ignored.
.PP
@@ -347,6 +806,25 @@ 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
+.PP
+Publish a year range as a static tree, then keep it in step with
+.B \-\-prune
+as the range moves:
+.RS
+.nf
+
+.B colitur publish \-\-from 2026 \-\-to 2027 \-\-out ~/public/colitur
+.B colitur publish \-\-from 2027 \-\-to 2028 \-\-out ~/public/colitur \-\-prune
+.fi
+.RE
.SH SOURCES
The calendar is computed against the 1962
.I Missale Romanum
@@ -376,6 +854,13 @@ reading citations fall back to the day's ordinary ones.
for the overlay file format \(em every directive, every field, the three date
shapes and worked examples.
.PP
+.BR colitur\-templates (5)
+for the template format used by
+.BR table ", " render " and " publish
+\(em the four syntax forms, the six flavours and their escaping, the full
+view\-model field reference, and the scope\-shadowing hazard a template author
+will hit.
+.PP
.BR lectio (1)
.SH LICENSE
AGPL\-3.0\-or\-later.