diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 19:22:40 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 19:22:40 +0200 |
| commit | d5d175118138249fe7d9e85d1bc65fb0b7e3a398 (patch) | |
| tree | 232c9c709b378acefff08bd7e7d930a7edb861fc | |
| parent | a9faf42cfbb1c060ac69f703f0c78616656566cb (diff) | |
| download | lectio-d5d175118138249fe7d9e85d1bc65fb0b7e3a398.tar.gz lectio-d5d175118138249fe7d9e85d1bc65fb0b7e3a398.zip | |
caldata: allow an external universal sanctorale to override the embedded one
The universal calendar of saints can now be refreshed or replaced without a
rebuild: if ~/.config/lectio/sanctorale/of.ini (OF) or ef.ini (EF) is present
and parses, caldata.Base uses it instead of the embedded calendar; a missing or
malformed file falls back to the embedded default, so the binary stays
self-contained and correct out of the box. Local diocesan/national additions
keep layering on top via calendars/ + `use` (unchanged). The temporal cycle and
precedence rules stay in code -- only the sanctorale is data.
- caldata.SetSanctoraleDir hook (mirrors bible/naming/i18n); Base() prefers the
external file per form. config.SanctoraleDir(); wired in the CLI, TUI and web
entry points. Tests cover override, no-file, and malformed-file fallback.
- README: new "Calendar of saints" section (two tiers: overlay via `use`,
replace via sanctorale/<form>.ini); config comment notes the override.
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | cmd/lectio-ui/main.go | 4 | ||||
| -rw-r--r-- | cmd/lectio-web/main.go | 4 | ||||
| -rw-r--r-- | internal/caldata/caldata.go | 27 | ||||
| -rw-r--r-- | internal/caldata/sanctorale_test.go | 47 | ||||
| -rw-r--r-- | internal/cli/cli.go | 4 | ||||
| -rw-r--r-- | internal/config/config.go | 17 |
7 files changed, 123 insertions, 1 deletions
@@ -297,6 +297,27 @@ data — no external service: Switch per run with `--lectionary new|traditional`, or set `lectionary` in config for a permanent default. +## Calendar of saints + +The universal sanctorale (saints, ranks, colours, names) is embedded and +oracle-validated, so it works out of the box. Because the Church changes it over +time, it can be updated or customised without a rebuild, in two tiers — both plain +INI: + +- **Local additions overlay on top.** Drop `~/.config/lectio/calendars/<name>.ini` + and enable it with `use = <name>` (comma list, later wins). A layer **adds** a + saint, **overrides** any field of an existing one (rank, colour, name, readings), + or **suppresses** it (`suppress = true`). This is the place for diocesan/national + propers and one-off rank fixes — you only write the deltas. +- **Replace the whole base.** Drop `~/.config/lectio/sanctorale/of.ini` (Ordinary + Form) or `ef.ini` (Extraordinary Form) and it is used *instead of* the embedded + calendar — for shipping a full refreshed calendar (regenerate with + `scripts/gen-sanctoral.go`). A missing or malformed file falls back to the + embedded default, so the tool stays self-contained and correct. + +The temporal cycle and the precedence rules (Table of Liturgical Days) are +computed in code — only the sanctorale is data. + ## Scripture corpora Only the Latin **Vulgate** (`vul`) — public domain, lectio's own hand-curation — diff --git a/cmd/lectio-ui/main.go b/cmd/lectio-ui/main.go index ad3d4b5..e7e7afd 100644 --- a/cmd/lectio-ui/main.go +++ b/cmd/lectio-ui/main.go @@ -12,6 +12,7 @@ import ( "github.com/lukaszkasprzak/lectio/internal/bible" "github.com/lukaszkasprzak/lectio/internal/bookmarks" + "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/naming" @@ -75,6 +76,9 @@ func run(args []string, stdout, stderr io.Writer) int { if dir, err := config.UIDir(); err == nil { i18n.SetUserDir(dir) } + if dir, err := config.SanctoraleDir(); err == nil { + caldata.SetSanctoraleDir(dir) + } var all, offline, reader bool var bibleVer, lectionary string diff --git a/cmd/lectio-web/main.go b/cmd/lectio-web/main.go index 59ea1e7..fb7f185 100644 --- a/cmd/lectio-web/main.go +++ b/cmd/lectio-web/main.go @@ -8,6 +8,7 @@ import ( "os" "github.com/lukaszkasprzak/lectio/internal/bible" + "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/naming" @@ -60,6 +61,9 @@ func run(args []string, stdout, stderr io.Writer) int { if dir, err := config.UIDir(); err == nil { i18n.SetUserDir(dir) } + if dir, err := config.SanctoraleDir(); err == nil { + caldata.SetSanctoraleDir(dir) + } var offline bool var port int diff --git a/internal/caldata/caldata.go b/internal/caldata/caldata.go index 9c17035..4e54f1f 100644 --- a/internal/caldata/caldata.go +++ b/internal/caldata/caldata.go @@ -77,9 +77,34 @@ func Tridentine() calendar.Layer { return l } +// sanctoraleDir, when set, holds an optional external universal sanctorale that +// overrides the embedded one (of.ini for OF, ef.ini for EF). Set once at startup +// via SetSanctoraleDir; empty means "use the embedded calendar". +var sanctoraleDir string + +// SetSanctoraleDir points caldata at the dir holding an optional external +// universal sanctorale (<dir>/of.ini, <dir>/ef.ini). Empty disables it. +func SetSanctoraleDir(dir string) { sanctoraleDir = dir } + // Base returns the universal base layer for a form: the 1962 calendar for -// "old" (EF), the General Roman Calendar otherwise (OF). +// "old" (EF), the General Roman Calendar otherwise (OF). If an external +// sanctorale file is present and parses, it REPLACES the embedded calendar for +// that form -- so the saints and ranks can be updated without a rebuild. A +// missing or malformed external file falls back to the embedded default, which +// therefore always keeps the tool self-contained and correct out of the box. func Base(form string) calendar.Layer { + if sanctoraleDir != "" { + name := "of.ini" + if form == "old" { + name = "ef.ini" + } + if data, err := os.ReadFile(filepath.Join(sanctoraleDir, name)); err == nil { + if l, err := ParseLayer(data); err == nil { + return l + } + // malformed external file: fall through to the embedded default + } + } if form == "old" { return Tridentine() } diff --git a/internal/caldata/sanctorale_test.go b/internal/caldata/sanctorale_test.go new file mode 100644 index 0000000..610aa33 --- /dev/null +++ b/internal/caldata/sanctorale_test.go @@ -0,0 +1,47 @@ +package caldata + +import ( + "os" + "path/filepath" + "testing" +) + +func TestBaseSanctoraleOverride(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "of.ini"), []byte( + "[layer]\nid = test-override\ntype = universal\n\n"+ + "[test-saint]\ndate = 06-15\nrank = feast\nname.en = Test Saint\n"), 0o644) + SetSanctoraleDir(dir) + t.Cleanup(func() { SetSanctoraleDir("") }) + + // External file present and valid -> it REPLACES the embedded OF calendar. + l := Base("new") + if l.ID != "test-override" { + t.Fatalf("external of.ini not used: layer id = %q", l.ID) + } + if _, ok := l.Cels["test-saint"]; !ok || len(l.Cels) != 1 { + t.Fatalf("external calendar not loaded verbatim: %d cels", len(l.Cels)) + } + + // EF form has no external file here -> embedded Tridentine still used. + if ef := Base("old"); ef.ID == "test-override" { + t.Fatal("EF wrongly took the OF override") + } +} + +func TestBaseFallsBackToEmbedded(t *testing.T) { + // No dir set -> embedded default. + SetSanctoraleDir("") + if l := Base("new"); l.ID != "universal" || len(l.Cels) < 100 { + t.Fatalf("no-override should give the embedded calendar, got id=%q n=%d", l.ID, len(l.Cels)) + } + + // A malformed external file must not break computation -> embedded fallback. + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "of.ini"), []byte("[unclosed section\n"), 0o644) + SetSanctoraleDir(dir) + t.Cleanup(func() { SetSanctoraleDir("") }) + if l := Base("new"); l.ID != "universal" || len(l.Cels) < 100 { + t.Fatalf("malformed override should fall back to embedded, got id=%q n=%d", l.ID, len(l.Cels)) + } +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 16ba753..c0c9dea 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -15,6 +15,7 @@ import ( "time" "github.com/lukaszkasprzak/lectio/internal/bible" + "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/export" "github.com/lukaszkasprzak/lectio/internal/i18n" @@ -210,6 +211,9 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { if dir, err := config.UIDir(); err == nil { i18n.SetUserDir(dir) } + if dir, err := config.SanctoraleDir(); err == nil { + caldata.SetSanctoraleDir(dir) + } if lectionary != "" { cfg.Lectionary = lectionary } diff --git a/internal/config/config.go b/internal/config/config.go index 08c0b60..0596194 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,6 +49,8 @@ const configHeader = `# lectio configuration (INI). Full-line comments only (# o # pager shell command, e.g. less -R; empty = off # use calendar layers to stack over the universal calendar: names of # ~/.config/lectio/calendars/<name>.ini files (comma list, order = precedence) +# (to REPLACE the whole universal calendar instead, drop +# ~/.config/lectio/sanctorale/of.ini or ef.ini -- see the README) # # [calendar] — computed-calendar placement options (defaults = Universal Roman Calendar): # epiphany fixed | sunday (fixed = Jan 6; sunday = the Sunday of Jan 2-8) @@ -360,6 +362,21 @@ func UIDir() (string, error) { return filepath.Join(filepath.Dir(p), "ui"), nil } +// SanctoraleDir is the user directory for an optional external universal +// sanctorale that REPLACES the embedded one: <dir>/of.ini (Ordinary Form) and +// <dir>/ef.ini (Extraordinary Form). It lets the calendar of saints and their +// ranks be refreshed as the Church changes them without rebuilding the binary; +// absent (the default), the embedded, oracle-validated calendar is used. Local +// diocesan/national additions layer ON TOP via calendars/ + `use` (they need no +// full file). Mirrors CalendarsDir. +func SanctoraleDir() (string, error) { + p, _, err := configPath() + if err != nil { + return "", err + } + return filepath.Join(filepath.Dir(p), "sanctorale"), nil +} + // NamesDir is the user directory for drop-in day-name language files // (<code>.ini), alongside calendars/ and corpora/ under the lectio config dir. // A file here overrides the built-in day names for its language key by key; |
