.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: an inner key silently loses to an outer key of the SAME NAME. 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, inside .BR {{#days}} , 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 flat booklet, dozens of wrong lines; in a month grid, EVERY cell reads the month's own name. .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, one line per day, using the safe name idiom from .BR "SCOPE AND LOOKUP" : .RS .nf {{rite}} {{year}} {{#days}} {{iso}} {{#name}}{{la}}{{^la}}{{slug}}{{/la}}{{/name}} {{colour}}{{#comms}} +{{slug}}{{/comms}} {{/days}} .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. A template using the unsafe, bare .B {{name.la}} form would instead have printed .RB \(lq Ianuarius \(rq on BOTH of those lines \(em the enclosing month's own name \(em see .B SCOPE AND LOOKUP above. .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.