summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 19:22:40 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 19:22:40 +0200
commitd5d175118138249fe7d9e85d1bc65fb0b7e3a398 (patch)
tree232c9c709b378acefff08bd7e7d930a7edb861fc /internal
parenta9faf42cfbb1c060ac69f703f0c78616656566cb (diff)
downloadlectio-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.
Diffstat (limited to 'internal')
-rw-r--r--internal/caldata/caldata.go27
-rw-r--r--internal/caldata/sanctorale_test.go47
-rw-r--r--internal/cli/cli.go4
-rw-r--r--internal/config/config.go17
4 files changed, 94 insertions, 1 deletions
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;