diff options
Diffstat (limited to 'man')
| -rw-r--r-- | man/colitur-templates.5 | 788 | ||||
| -rw-r--r-- | man/colitur.1 | 501 |
2 files changed, 1281 insertions, 8 deletions
diff --git a/man/colitur-templates.5 b/man/colitur-templates.5 new file mode 100644 index 0000000..2f42d68 --- /dev/null +++ b/man/colitur-templates.5 @@ -0,0 +1,788 @@ +.TH COLITUR\-TEMPLATES 5 "2026" "colitur" "File Formats" +.SH NAME +colitur\-templates \- template format for colitur(1)'s table, render and publish +.SH SYNOPSIS +.I booklet.tex +.br +.I calendar.html +.br +.I feed.ics +.SH DESCRIPTION +A +.B colitur +template is the file named by +.BR "colitur table" 's +and +.BR "colitur render" 's +.B \-\-template +flag (and used internally by +.BR "colitur publish" ). +It is a deliberately logic\-less, Mustache\-family format: the file is +.I data, +never a program. There are exactly four constructs \(em variable +interpolation, a section, an inverted section, and a comment \(em and nothing +else. +.PP +.B There are no partials, no lambdas, no arithmetic, no expression +.B evaluation, and no "raw" or triple\-brace form that could opt out of +.B escaping. +A template cannot include another file, cannot compute anything, and cannot +choose to skip the escaping its own flavour applies. Everything a rendered +document needs \(em conditionals on emptiness, iteration over days or weeks, +formatting \(em is expressed with the four constructs below over the fields +.B VIEW MODEL +describes; nothing else is available, and nothing else will be added by +supplying cleverer template syntax \(em that is what the escaping and +scope rules exist to prevent. +.SH SYNTAX +.TP +.BI "{{" name "}}" +Interpolates the value at +.I name, +a dot\-separated path resolved against the current scope (see +.B SCOPE AND LOOKUP +below). A string value is escaped per the active flavour and inserted; a +boolean +.B true +renders as the literal text +.RB \(lq true \(rq, +.B false +renders as nothing; a list or an object value used as a plain variable also +renders as nothing \(em only a section can iterate one. A path that resolves +to nothing renders as nothing, silently: this is the one deliberate silence +in the engine, so a template survives a day that does not carry every +optional field (an empty +.I week +on a day the rite does not number, an empty +.I first +or +.I gospel +citation, and so on). +.TP +.BI "{{#" name "}}...{{/" name "}}" +A section. If +.I name +resolves to a +.B list, +the body is rendered once per item, with each item pushed onto the scope +stack (see below). If it resolves to a truthy non\-list value (a non\-empty +string, or an object), the body is rendered once, with that value pushed onto +the stack. If it resolves to nothing, or to a falsy value (an empty string, +.BR false , +or an empty list), the body is skipped entirely. +.TP +.BI "{{^" name "}}...{{/" name "}}" +An inverted section: the mirror image of +.BR # . +The body renders \(em exactly once, without pushing anything new onto the +scope \(em only when +.I name +resolves to nothing, or to a falsy value. This is how a template supplies a +fallback for an optional or absent field. +.TP +.BI "{{!" " text " "}}" +A comment. Everything between +.B {{! +and the closing +.B }} +is discarded; nothing is written to the rendered output. See +.B HOST\-LANGUAGE COMMENTS +below before relying on this for documentation inside a template that also +has its own comment syntax. +.PP +A section and its inverted counterpart, and a section and its close tag, must +name the identical path \(em +.BR {{#days}} " ... " {{/months}} +is a parse error, not a silently mismatched close. An empty path +( +.BR {{.}} ", " {{#}} ", " {{^}} ", " {{/}} +) is also a parse error: there is no "current context" concept for a bare dot +to mean, so nothing is guessed on a template's behalf. +.PP +A malformed template \(em an unterminated +.BR {{ , +a section left unclosed, a close tag with no matching open \(em is reported +with the parser's own reason and exits 2. A template is user input, exactly +like an +.BR colitur\-overlay (5) +file, and is never allowed to crash the program that reads it. +.SH SCOPE AND LOOKUP +.B This section documents a real hazard, not a theoretical one \(em it has +.B produced wrong output during this program's own development. +.PP +Scope is a stack. Rendering starts with the whole view (the year) as the one +entry on the stack; each +.B {{#section}} +pushes the value it iterates or opens onto the stack for the duration of its +body, and pops it again at +.BR {{/section}} . +A lookup for +.I name +is tried against the +.I innermost +(most recently pushed) entry first. If +.I name +is not found there, the lookup falls back to the +.I next +entry outward, and so on to the outermost (the year itself). This fallback is +deliberate and necessary \(em without it, a cell deep inside +.B {{#months}}{{#weeks}}{{#days}} +could never reach +.BR {{year}} , +which lives only on the outermost object. +.PP +.B The hazard: a DOTTED PATH that resolves only PART WAY inward falls back +.B WHOLESALE to an outer scope of the SAME NAME. +A bare key inside the immediately enclosing section is not at risk this way +\(em it either resolves right there or fails outright (see +.B num +below); it is specifically a dotted path, one step of which is missing from +the inner object, that abandons the whole path and restarts the lookup one +level out. Falling back outward means a name that exists at +.I both +levels never fails and never warns \(em it just silently resolves to the +.I outer +one, because the inner object's own absence of that key is indistinguishable +from "look further out" and "this key does not apply here". Two collisions +are known to exist in the shipped view model: +.TP +.B name +Both a +.B month +and a +.B day +carry a +.I name +field (each an object keyed by language, e.g. +.BR la " and " en ). +Written naively, when +.B {{#days}} +is nested inside +.BR {{#months}} , +a bare +.B {{name.la}} +does +.I not +resolve to the day's own Latin name. It resolves to the +.I enclosing month's +Latin name, because the day's own +.I name +object either has no +.B la +key (an unnamed day) or the dotted path fails partway and the WHOLE path +falls back to the outer scope, which does have one. This is not a corner +case: on an ordinary month, most days carry no Latin name at all (only named +sanctoral days do), so the naive form renders the +.I month's +name on nearly every day \(em in a per\-month booklet, dozens of wrong +lines; in a month grid (whose +.B {{#weeks}} +is itself only ever reachable through +.BR {{#months}} , +since +.I weeks +is a field of +.IR month , +never a top\-level list) EVERY cell reads the month's own name. +.PP +.B The collision needs a month actually on the scope stack to fire. +Iterating the TOP\-LEVEL, flat +.B days +list directly ( +.B {{#days}}...{{/days}} +at the outermost level, never passing through +.B {{#months}} +first) puts no month object anywhere on the stack, so the naive form does +.I not +silently substitute the wrong answer there \(em it silently resolves to +nothing, exactly as any other absent key would, because there is no +outer scope left to climb to. This is precisely what makes the hazard easy +to miss: an author who tries the naive form against the flat list first +sees the unnamed days come out empty, reasonably concludes the form is +safe, and then hits the real collision the moment the identical fields are +read from inside +.BR {{#months}} , +which is what every shipped template that produces a grid or a per\-month +booklet actually does. +.TP +.B num +Both a +.B month +and a +.B week +carry a +.I num +field. Inside +.BR {{#weeks}} , +a bare +.B {{num}} +is the week's own number, correctly \(em but only because nothing between +the week and the day currently redefines it. A template that reaches +.I num +from any scope where the immediately enclosing section does not itself +carry it will silently climb to whichever ancestor does, and that may not be +the one the author meant. +.PP +.B The safe idiom. +Push the object you actually want onto the scope stack yourself, with a +.B {{#name}} +section, before reading its fields \(em then a bare field inside that +section can only resolve against the object you just pushed, or fail +outright and fall through to an inverted fallback you write explicitly: +.RS +.nf + +{{#name}}{{la}}{{^la}}{{slug}}{{/la}}{{/name}} +.fi +.RE +This opens the day's own +.I name +object (never the month's \(em a +.B {{#section}} +always resolves its OWN path from the point it appears, which for +.B {{#name}} +written inside +.B {{#days}} +is the day's +.IR name , +shadowing the month's identically\-named field exactly as intended), reads +.B la +from it, and falls back to the day's own +.B slug +only when +.B la +is genuinely absent from +.I that +object \(em never when it is merely absent from an ancestor. Every shipped +template that prints a day's name uses exactly this idiom; none uses the +bare dotted form. +.SH HOST\-LANGUAGE COMMENTS +.B The engine has no awareness of the target language's own comment syntax. +A +.B {{...}} +tag inside a LaTeX +.BR % , +a groff +.BR .\e" , +or an HTML +.B <!\-\- \-\-> +comment is still lexed and rendered exactly as if it were live template +markup \(em the engine sees only its own +.B {{ +/ +.B }} +delimiters, never the host format's comment sigils, because a template is +rendered as one flat character stream, not parsed as LaTeX, groff or HTML +first. This broke a shipped template during development: an explanatory +.B {{example}} +written inside a LaTeX +.B % +comment, meant purely as documentation for a future reader, was parsed as a +real variable reference. +.PP +Write in\-template documentation without any +.B {{ +or +.B }} +characters in it, in whatever host\-comment syntax the target format uses. +Use +.B {{!comment}} +only where the surrounding host format has no comment syntax of its own that +would otherwise be preferable (its own body is safe \(em text between +.B {{! +and +.B }} +is discarded unparsed, so a stray +.B {{ +inside a +.B {{!...}} +comment is not itself a hazard \(em but the comment's own delimiters are +still ordinary +.B {{ +/ +.B }} +tokens, so they compete with the host format's own comment syntax for the +same file exactly as any other tag would). +.SH FLAVOURS +.B \-\-flavour +selects how an interpolated +.I value +is escaped before being written. It never touches the template's own literal +markup (the LaTeX, groff, HTML, XML or ICS surrounding a +.BR {{tag}} ), +which is the template author's and is trusted exactly as written. One of six: +.TP +.B latex +.BR \e " \(-> " \etextbackslash{} , +.BR { " \(-> " \e{ , +.BR } " \(-> " \e} , +.BR $ " \(-> " \e$ , +.BR & " \(-> " \e& , +.BR # " \(-> " \e# , +.BR _ " \(-> " \e_ , +.BR % " \(-> " \e% , +.BR ^ " \(-> " \etextasciicircum{} , +.BR ~ " \(-> " \etextasciitilde{} . +Every LaTeX special character is covered; nothing else is touched. +.TP +.B groff +A backslash is escaped to +.BR \ee , +because a bare backslash starts a groff escape. If the ESCAPED string then +begins with +.B . +or +.BR ' , +the zero\-width non\-printing character +.B \e& +is prefixed \(em a +.B . +or +.B ' +in column one would otherwise start a request rather than print literally. +.TP +.B html +(also used for the +.B xml +flavour, identically) +.BR & " \(-> " & , +.BR < " \(-> " < , +.BR > " \(-> " > , +.BR \(dq " \(-> " " , +.BR ' " \(-> " ' . +.TP +.B xml +Identical to +.B html +above. +.TP +.B ics +Per RFC 5545: a backslash doubles, a semicolon and a comma are each +backslash\-escaped, a newline becomes the two\-character sequence +.BR \en , +and a carriage return is dropped outright (never doubled or passed through). +Line folding at 75 octets, on a UTF\-8 character boundary, is applied +separately to the whole rendered line \(em it is not part of value escaping +and is not something a template can see or control. +.TP +.B none +Escapes nothing at all: the value is inserted byte\-for\-byte. See +.B LIMITATIONS +below \(em this is not an oversight, and it is not safe to treat as one. +.PP +.B \-\-flavour +is inferred from +.BR \-\-template 's +own file extension when the flag is omitted: +.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 +An extension +.B colitur +does not recognise is a hard +.B ERROR +naming the six flavours above; it is +.I never +a silent fallback to +.BR none . +Guessing the flavour wrong would produce output that looks fine right up +until the metacharacters it silently failed to escape appear in a rendered +document. +.SH LIMITATIONS +.B AsciiDoc and Markdown are NOT escaped. +The +.B none +flavour (selected for +.IR .md " and " .adoc , +as well as +.IR .txt ) +passes every interpolated value through unchanged. This is a deliberate +choice, not a gap: unlike LaTeX, groff, HTML, XML or ICS, AsciiDoc and +Markdown have no fixed, small metacharacter set that could be escaped +mechanically \(em their own metacharacters are context\-dependent (a +.B * +means something different at the start of a line than in the middle of a +word), and escaping them here, generically, would produce +.I worse +output than leaving values alone in the ordinary case. +.PP +The consequence is real and is stated here plainly rather than left for a +reader to discover: a feast name, or any other interpolated field, containing +.B * +or +.B _ +renders as Markdown/AsciiDoc emphasis in the rendered document, not as a +literal asterisk or underscore. No shipped sanctoral name currently contains +either character, but a +.BR colitur\-overlay (5) +file supplying a local celebration's own name is not validated against this +constraint, and its author is responsible for avoiding both characters, or +accepting the emphasis, in any name rendered through a +.I .md +or +.I .adoc +template. +.PP +Only the Extraordinary Form (1962) view model is documented below; see +.BR colitur (1) +for the rite's own scope and limitations (readings cover only the Epistle +and Gospel; the votive Office of the Blessed Virgin Mary on Saturday does not +yet select among its five seasonal Masses). +.SH VIEW MODEL +The value a template renders against is built once per +.B colitur table +/ +.B render +/ +.B publish +invocation, from the same resolved calendar +.B colitur emit +uses, and is shaped for two artefacts from one model: a flat booklet (the +.B days +list, one entry per day of the requested year) and a month grid (the +.B months +list, each carrying its own +.B weeks +list of Sunday\-started, seven\-cell rows, padded at both ends with blank +cells so every row has exactly seven). This is the same shape published at +.IR schema/day\-v1.json , +described here in prose; the JSON Schema is the machine\-checked contract and +this page is its worked explanation. +.SS Top level +.TP +.B rite +The rite identifier, currently always the string +.BR ef . +.TP +.B year +The civil year requested, as a four\-digit string. +.TP +.B months +A list of twelve +.B month +objects, January through December. +.TP +.B days +A flat list of every day's own +.B day +object, in date order, for the whole requested year \(em what a booklet +template iterates over directly, without going through +.BR months . +.SS month +.TP +.B num +The month number, 1 through 12, as a string. +.TP +.B name +An object keyed by language (currently +.B la +and +.BR en ), +each value the month's own name in that language (e.g. +.RB \(lq Ianuarius \(rq +/ +.RB \(lq January \(rq ). +.TP +.B days +This month's own +.B day +objects, in date order, only the days that actually fall in this month. +.TP +.B weeks +This month's +.B day +objects grouped into Sunday\-started rows of exactly seven, the first and +last rows padded with blank cells (see +.B day \(-> in_month +below) so every row has seven entries regardless of which weekday the month +starts or ends on. +.SS week +.TP +.B num +The week's ordinal within its month (1, 2, 3, ...), as a string. This is +.I not +a liturgical week number \(em see +.B day \(-> week +below for that. +.TP +.B days +Exactly seven +.B day +objects, Sunday first. +.SS day +Every key below is always present on every day object, including a padding +cell (see +.BR in_month ), +so a template never hits a missing key on a real day OR a blank grid cell \(em +the one deliberate exception is that a padding cell's own string fields are +all set to the empty string and its boolean and list fields to +.B false +/empty, which read as absent under +.BR # / ^ / {{var}} +exactly as a genuinely unset field would. +.TP +.B iso +The date, ISO\-8601 (\c +.IR YYYY\-MM\-DD ). +Empty on a grid padding cell. +.TP +.B dom +The day of the month, as a string (no leading zero). Empty on a padding +cell. +.TP +.B dow +The day of the week as a string digit, +.B 0 +for Sunday through +.B 6 +for Saturday. Present, and meaningful, even on a padding cell \(em it is how +a grid template knows which column a blank cell belongs in. +.TP +.B in_month +Boolean. +.B false +on a padding cell (a blank cell added so a month's first or last week has +seven entries); a template checks this, not +.BR iso 's +emptiness, to decide whether to render a cell's contents. +.TP +.B season +The liturgical season's own string name (e.g. +.BR paschaltide ", " lent ). +.TP +.B week +The liturgical week number within the season, as a string, or the empty +string on a day the rite does not number (this is +.I not +the same field as a +.B week +object's own +.BR num , +described above \(em see +.B SCOPE AND LOOKUP +for why the two identically\-named fields do not collide here: a plain +.B day +object has no +.B num +key of its own at all, only +.BR week , +so there is nothing for it to shadow). +.TP +.B slug +The observed celebration's stable identifier (e.g. +.BR ef\-easter\-sunday ). +.TP +.B name +An object keyed by language, the observed celebration's own name in each +language colitur's data supplies one for. Frequently has no +.B la +or +.B en +key at all (most temporal days, most sanctoral entries in the shipped data) +\(em see +.B SCOPE AND LOOKUP +above for the resulting month\-name collision and its safe idiom. +.TP +.B rank +The observed celebration's class, as the kernel's own string (e.g. +.BR class\-1 ). +There is deliberately no separate, localized rank label: the kernel carries +no per\-language rank names to draw one from. +.TP +.B colour +The observed celebration's liturgical colour, lowercase (one of +.BR white ", " red ", " green ", " violet ", " rose ", " black , +or the empty string). +.TP +.BR is_white ", " is_red ", " is_green ", " is_violet ", " is_rose ", " is_black +Six booleans, exactly one true (matching +.BR colour ) +on a real day, all false on a padding cell. Provided so a template can +select styling (a cell background colour, a class name) with a plain +.B {{#is_white}} +section instead of a string comparison the engine does not offer \(em there +is no expression evaluation, so this is the only way a template branches on +colour at all. +.TP +.B subject +Whose feast this is, lowercase (one of +.BR lord ", " bvm ", " saint ", " temporal ). +.TP +.B comms +A list of commemoration objects admitted on this day, each carrying +.BR slug , +.B name +(an object keyed by language, same shape as the day's own +.BR name ), +and +.B privileged +(boolean: true for a privileged commemoration under RG 109, which survives +even where an ordinary one would be capped out). Empty list on a day with no +commemorations, and always an empty list \(em never absent \(em on a padding +cell. +.TP +.B transferred_in +A list of at most one object, present when a feast impeded elsewhere was +transferred onto THIS day (RG 96\(en98); carries the transferred +celebration's own +.BR slug . +Empty list when nothing transferred in. +.TP +.B transferred_out +A list of objects, one per celebration that would have fallen on this day +but was displaced and moved to a later date; each carries +.B slug +and +.B to +(the ISO\-8601 date it was moved to). Empty on the ordinary day. +.TP +.B first +The Epistle/Lesson reading citation (e.g. +.RB \(lq "Heb 1:1\-12" \(rq ), +never scripture text \(em a reference only. Empty string when none resolved. +.TP +.B gospel +The Gospel reading citation, same shape as +.BR first . +.TP +.B last +Boolean, true on the seventh (final) cell of a grid row, false everywhere +else including every entry of the flat +.B days +list. Exists because the engine offers no "unless this is the last item" +construct, so a template that must print a separator BETWEEN cells but not +after the last one (a table row's column rule, for instance) reads this flag +rather than computing it: without it, a seven\-column LaTeX grid would emit +an eighth, empty column and +.B pdflatex +would reject the file outright. +.SH A WORKED MINIMAL TEMPLATE +A plain\-text booklet, days nested inside months \(em the shape every +shipped template actually uses, and the shape the +.B SCOPE AND LOOKUP +hazard needs to fire \(em using the safe name idiom from that section: +.RS +.nf + +{{rite}} {{year}} +{{#months}}{{#days}} +{{iso}} {{#name}}{{la}}{{^la}}{{slug}}{{/la}}{{/name}} {{colour}}{{#comms}} +{{slug}}{{/comms}} +{{/days}}{{/months}} +.fi +.RE +.PP +Rendered (extension +.IR .txt , +so flavour +.BR none , +no escaping applied; the blank lines are the template's own \(em its section +body starts and ends with a literal newline, and this engine does not trim +one): +.RS +.nf + +.B colitur table \-\-year 2026 \-\-template minimal.txt | head \-7 +ef 2026 + +2026\-01\-01 ef\-circumcision white + +2026\-01\-02 ef\-christmas\-1\-friday white + +2026\-01\-03 Officium sanctae Mariae in sabbato white +.fi +.RE +.PP +The second data line shows the fallback firing: 2 January carries no Latin +.B name +in the shipped data, so +.B {{^la}} +supplies +.B {{slug}} +instead. The third shows the non\-fallback case: 3 January +.I does +carry a Latin name (the votive Office of the Blessed Virgin Mary on +Saturday), and the idiom prints it correctly. +.PP +Now swap only the interpolation \(em the safe idiom above becomes the +naive, unsafe +.B {{name.la}} , +nesting left exactly as it was: +.RS +.nf + +{{rite}} {{year}} +{{#months}}{{#days}} +{{iso}} {{name.la}} {{colour}} +{{/days}}{{/months}} +.fi +.RE +.PP +Rendered against the identical year, same extension and flavour: +.RS +.nf + +.B colitur table \-\-year 2026 \-\-template minimal\-unsafe.txt | head \-7 +ef 2026 + +2026\-01\-01 Ianuarius white + +2026\-01\-02 Ianuarius white + +2026\-01\-03 Officium sanctae Mariae in sabbato white +.fi +.RE +.PP +The collision fires for real on the first two lines: with a month on the +scope stack, the day's own missing +.B la +key falls back all the way out to the ENCLOSING MONTH's own +.RB \(lq Ianuarius \(rq +rather than failing. The third line is untouched, because 3 January's own +.I name +object genuinely has a +.B la +key, so the dotted path resolves without ever needing to fall back. +.PP +.B This is specific to nesting, and that is the point. +The identical naive +.B {{name.la}} +written against the TOP\-LEVEL, flat +.B days +list (no +.B {{#months}} +wrapping it) does +.I not +print +.RB \(lq Ianuarius \(rq +anywhere \(em there is no month on the stack to fall back to, so it prints +nothing on an unnamed day instead, silently but not wrongly. See +.B SCOPE AND LOOKUP +above for why testing the naive form only against a flat list is exactly +how this hazard goes unnoticed until a template is later nested under +.BR {{#months}} . +.SH SEE ALSO +.BR colitur (1) +for +.BR table ", " render " and " publish , +and for the five +.B emit +formats that share this same view model. +.PP +.BR colitur\-overlay (5) +for the local\-calendar file format that supplies the celebrations a +template's +.B name +and +.B slug +fields can carry. +.SH LICENSE +AGPL\-3.0\-or\-later. 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. |
