1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
|
.TH COLITUR 1 "2026" "colitur" "User Commands"
.SH NAME
colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, 1962)
.SH SYNOPSIS
.B colitur
.BR easter | temporal | day | readings
.I YEAR
.br
.B colitur
.BR day | readings
.I YEAR
.BI \-\-overlay " FILE"
.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
.BR \-h | \-\-help
.SH DESCRIPTION
.B colitur
computes the liturgical calendar of the 1962 Roman Missal \(em the
Extraordinary Form \(em and the Mass reading citations for every day, entirely
offline and without a network. Its name is
.I computus liturgicus
crossed with the Latin
.IR colitur ,
"He is worshipped".
.PP
For each day it resolves the season, the week, the observed celebration with
its rank and colour, any commemorations, any transfers, and the day's Epistle
and Gospel. It emits reading
.I references
only \(em
.RB \(lq "Jn 3:16" \(rq
\(em and never scripture text.
.PP
The engine is total and deterministic over the whole domain
.BR "1583..9999" .
It reads no clock, draws no randomness, and given the same data produces the
same answer for any year in range. Years outside the domain are refused at the
boundary rather than approximated.
.PP
Each report covers 1 January to 31 December of the civil
.IR YEAR .
That is deliberately not a liturgical year, which is Advent\-anchored and
straddles two civil years;
.B colitur
resolves both liturgical years that touch the requested civil one and prints
the civil slice.
.SH COMMANDS
.TP
.BI easter " YEAR"
Easter and the movable feasts anchored to it \(em Ash Wednesday, Palm Sunday,
Ascension, Pentecost, Corpus Christi \(em one per line, as
.RI \(lq name " " date \(rq.
.TP
.BI temporal " YEAR"
The temporal cycle alone, one line per day, before the sanctoral calendar is
resolved against it. Chiefly useful for inspecting season and week boundaries
in isolation.
.TP
.BI day " YEAR"
The resolved day identity, one line per day: the temporal cycle and the
sanctoral calendar reconciled by the rite's own rules of precedence,
occurrence, commemoration and transfer.
.TP
.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
.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
parsed back with the same function that loads an overlay, and must mean
exactly what the INI said. A separate step rather than teaching
.B \-\-overlay
to sniff the extension, so you can see what your INI became. See
.BR colitur\-overlay (5).
.TP
.B new\-overlay
Print a starter overlay file to standard output, for redirection. Every value
in it is a placeholder that will appear in
.B day
output if left unedited, so a half\-finished overlay is visible rather than
silently inert.
.TP
.BI check " FILE" ...
Load each overlay, apply it to the shipped calendar, and report what it does:
the directive counts, the slug each targets, and any directive that matched
nothing. Exits
.B 2
if a file fails to load or a directive found no target, so it composes into a
Makefile or a pre\-commit hook. It answers three narrow questions \(em does
the file parse, does every directive find its target, and what does the merged
result contain. It does
.I not
validate a calendar against the rubrics, and cannot: see
.B OVERLAYS
below.
.TP
.BI \-\-overlay " FILE"
Apply a user calendar on top of the shipped one. Repeatable and ordered;
.BR day ", " readings ", " emit ", " table " and " render
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" " 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" " 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 \-\-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
.BR \-V ", " \-\-version
Print the version and exit 0.
.SH OUTPUT FORMAT
.SS day
.RS
.nf
date weekday season week slug rank colour [+commemoration ...]
.fi
.RE
.PP
Space\-separated, with one
.BI + slug
suffix per admitted commemoration. A
.B \-
in the week column means the day carries no week number.
.RS
.nf
2026\-04\-05 sunday paschaltide 1 ef\-easter\-sunday class\-1 white
2026\-11\-02 monday time\-after\-pentecost 23 commemoration\-of\-all\-souls class\-1 black
2057\-03\-26 monday lent 3 annunciation\-of\-the\-blessed\-virgin\-mary class\-1 white +ef\-lent\-3\-monday
.fi
.RE
.SS readings
.RS
.nf
date slug | Epistle | Gospel
.fi
.RE
.PP
A reading citation contains spaces and commas, so this report separates its
fields with
.RB \(lq " | " \(rq
where
.B day
stays space\-separated. That is the reason the citations are a separate command
rather than extra columns on
.BR day :
appended there, no field number could recover where the Epistle ended. A
.B \-
in either citation field means none was resolved.
.RS
.nf
2026\-12\-25 ef\-nativity | Heb 1:1\-12 | John 1:1\-14
2038\-03\-06 sts\-felicitas\-perpetua | Ecclus 51:1\-8, 12 | Matt 13:44\-52
.fi
.RE
.PP
Both reports are one line per day and ordered by date, so they compose with
.BR grep (1),
.BR awk (1)
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.
.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"
.RB ( \-o )
Apply a user\-supplied calendar on top of the shipped universal one. Repeatable
and ordered.
.PP
Overlays are applied
.I on top of
the 1962 universal calendar, never instead of it. The shipped adjustments \(em
which carry the inseparable Peter/Paul commemoration, the Major Litanies, St
Barbara and Rogation Wednesday \(em are applied first, then each
.B \-\-overlay
in the order given. Last writer wins, so a later file may deliberately override
an earlier one, or a universal entry, by naming its slug.
.PP
The workflow:
.RS
.nf
.B colitur new\-overlay > my\-parish.sexp
.B $EDITOR my\-parish.sexp
.B colitur check my\-parish.sexp
.B colitur day 2026 \-\-overlay my\-parish.sexp
.fi
.RE
.PP
The format is documented in full in
.BR colitur\-overlay (5).
In brief, an overlay is an S\-expression file with an
.I id
and a list of directives:
.BR Add ", " Suppress ", " Replace " and " Edit .
An added entry carries its own date specification, which may be a fixed
.RI ( month ", " day )
pair, an
.I Easter_offset
in days (signed; Easter itself is 0), or an
.I Nth_weekday
of a month \(em the
.I nth
may be negative to count from the end, so
.B \-1
is the last \(em so a patronal feast on "the first Sunday of October" or a
dedication anniversary reckoned from Easter are both expressible.
.PP
In an added celebration the
.I citations
and
.I layer
fields may be omitted: they default to empty and to the overlay's own
.IR id .
The remaining six are required, and each is a closed set \(em
.I rank
is
.BR Class1 ", " Class2 ", " Class3 " or " Class4 ,
.I status
is
.B Feast
or
.BR Commemoration_only ,
.I colour
is
.BR White ", " Red ", " Violet ", " Green ", " Black " or " Rose ,
and
.I subject
is
.BR Lord ", " Bvm ", " Saint " or " Temporal .
See
.I <prefix>/share/colitur/examples/diocesan\-example.sexp
for a worked, runnable example of all four directives and all three date
shapes:
.RS
.nf
.B colitur day 2026 \-\-overlay <prefix>/share/colitur/examples/diocesan\-example.sexp
.fi
.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
.I refused
rather than silently ignored.
.PP
.B An overlay is applied, not validated.
This program's test layers \(em properties over every year in the domain, a
differential against a sibling engine, two published\-calendar oracles, and
hand\-verified pins \(em assert things about the
.I shipped
calendar. None of them can vouch for a file you supply. A directive naming a
slug that does not exist prints a warning to standard error and the run
continues, so a typo in a local calendar is visible rather than silent; a file
that fails to load is fatal.
.SH ENVIRONMENT
.TP
.B COLITUR_DATA_DIR
Read the calendar data from this directory instead of the installed or
build\-tree location. If it is set and contains no
.IR sanctoral.sexp ,
.B colitur
exits 2 naming the directory; it does
.I not
fall back to another copy. Naming a directory states an intent, and quietly
computing a calendar from different data than the one requested is a failure
mode this program refuses.
.SH FILES
.TP
.I <prefix>/share/colitur/ef/
The installed calendar data: the sanctoral calendar, its one hand\-authored
overlay, the temporal lectionary and the Commons. Four S\-expression files.
.TP
.I <prefix>/share/colitur/examples/diocesan\-example.sexp
A worked example overlay, shipped as runnable documentation. Every
celebration in it is invented; copy it and put your own calendar in its place.
.TP
.I <exedir>/../data/ef/
The build\-tree location, used when running from a source checkout.
.PP
Resolution order is
.B COLITUR_DATA_DIR
first, then the installed directory, then the build tree. A directory counts
only if it actually contains
.IR sanctoral.sexp ,
so a failed or half\-removed installation falls through to a working checkout
instead of shadowing it.
.SH EXIT STATUS
.TP
.B 0
Success.
.TP
.B 2
Bad usage, a year outside 1583..9999, or the calendar data could not be read.
.SH EXAMPLES
Easter and its dependent feasts:
.RS
.nf
.B colitur easter 2026
.fi
.RE
.PP
One date:
.RS
.nf
.B colitur day 2026 | grep '^2026\-12\-25'
.fi
.RE
.PP
Every first\-class day of a year:
.RS
.nf
.B colitur day 2026 | awk '$6 == "class\-1"'
.fi
.RE
.PP
Days carrying at least one commemoration:
.RS
.nf
.B colitur day 2027 | grep '+'
.fi
.RE
.PP
A local calendar on top of the universal one:
.RS
.nf
.B colitur day 2026 \-\-overlay ~/calendars/diocese.sexp
.B colitur day 2026 \-\-overlay ~/calendars/diocese.sexp \-\-overlay ~/calendars/parish.sexp
.fi
.RE
.PP
Run against a checkout's data rather than the installed copy:
.RS
.nf
.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
and its
.IR "Rubricae Generales" ,
which are the sole authority for what
.B colitur
emits. Published calendars \(em missalemeum, Divinum Officium, gcatholic \(em
are used as comparison oracles in the test suite only: a divergence from one
is flagged loudly and adjudicated against the Missal, never silently adopted.
Several such divergences have been resolved in this engine's favour.
.SH LIMITATIONS
Only the Extraordinary Form (1962) is implemented; the Ordinary Form is a
planned peer module, not an overlay of this one.
.PP
Only the Epistle and the Gospel are emitted. The chants \(em Psalm, Gradual,
Tract, Alleluia, Sequence \(em are deliberately not computed: they have no
source in this engine's data and no oracle to validate them against, and the
engine rejects any citation part outside those two rather than emit one it
cannot stand behind.
.PP
The votive Office of the Blessed Virgin Mary on Saturday is kept, but the
seasonal selection among its five Masses is not yet implemented, so its
reading citations fall back to the day's ordinary ones.
.SH SEE ALSO
.BR colitur\-overlay (5)
for the overlay file format \(em every directive, every field, the three date
shapes and worked examples.
.PP
.BR lectio (1)
.SH LICENSE
AGPL\-3.0\-or\-later.
|