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
|
// Package caldata embeds and parses lectio's owned universal General Roman
// Calendar data into a calendar.Layer. It imports only internal/ini and
// internal/calendar (types).
package caldata
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"github.com/lukaszkasprzak/lectio/internal/calendar"
"github.com/lukaszkasprzak/lectio/internal/ini"
)
//go:embed roman-calendar.ini
var romanCalendar []byte
//go:embed tridentine-calendar.ini
var tridentineCalendar []byte
//go:embed tridentine-lectionary.ini
var tridentineLectionary []byte
// efTempReadings maps an EF temporal-day slug to its readings (parsed once).
var efTempReadings = parseLectionary(tridentineLectionary)
//go:embed of-lectionary.ini
var ofLectionary []byte
// ofTempReadings maps an OF <slug>-<cycle> key to its readings (parsed once).
var ofTempReadings = parseLectionary(ofLectionary)
func parseLectionary(data []byte) map[string][]calendar.Reading {
l, err := ParseLayer(data)
if err != nil {
panic(fmt.Sprintf("caldata: tridentine-lectionary.ini invalid: %v", err))
}
out := map[string][]calendar.Reading{}
for slug, rc := range l.Cels {
var rs []calendar.Reading
for _, part := range []string{"first", "psalm", "second", "gospel"} {
if c := rc.Fields[part]; c != "" {
rs = append(rs, calendar.Reading{Part: part, Citation: c})
}
}
out[slug] = rs
}
return out
}
// TemporalReadings returns the temporal-cycle readings for a computed day slug,
// or nil. EF ("old") looks up efTempReadings; OF looks up ofTempReadings.
func TemporalReadings(form, slug string) []calendar.Reading {
if form == "old" {
return efTempReadings[slug]
}
return ofTempReadings[slug]
}
// Universal parses the embedded Ordinary Form universal calendar. It panics on a
// malformed embedded file (a build-time bug caught by tests).
func Universal() calendar.Layer {
l, err := ParseLayer(romanCalendar)
if err != nil {
panic(fmt.Sprintf("caldata: embedded roman-calendar.ini invalid: %v", err))
}
return l
}
// Tridentine parses the embedded Extraordinary Form (1962) universal calendar.
func Tridentine() calendar.Layer {
l, err := ParseLayer(tridentineCalendar)
if err != nil {
panic(fmt.Sprintf("caldata: embedded tridentine-calendar.ini invalid: %v", err))
}
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). 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()
}
return Universal()
}
// LoadLayer reads and parses a user calendar layer file. id (the filename stem)
// is used as the layer's ID when the file's [layer] header omits one.
func LoadLayer(path, id string) (calendar.Layer, error) {
data, err := os.ReadFile(path)
if err != nil {
return calendar.Layer{}, err
}
l, err := ParseLayer(data)
if err != nil {
return calendar.Layer{}, fmt.Errorf("%s: %w", path, err)
}
if l.ID == "" {
l.ID = id
}
return l, nil
}
// Stack returns the ordered layer stack for the engine: the form's universal
// base first, then each user layer named in use (matched to "<dir>/<id>.ini"),
// in order. A layer that fails to load is skipped and reported in the error
// slice so a single bad file never breaks calendar computation.
func Stack(form, dir string, use []string) ([]calendar.Layer, []error) {
layers := []calendar.Layer{Base(form)}
var errs []error
for _, id := range use {
l, err := LoadLayer(filepath.Join(dir, id+".ini"), id)
if err != nil {
errs = append(errs, err)
continue
}
layers = append(layers, l)
}
return layers, errs
}
// ParseLayer parses INI bytes (a [layer] header + [slug]/[slug/variant]
// celebration sections) into a calendar.Layer.
func ParseLayer(data []byte) (calendar.Layer, error) {
secs, err := ini.Parse(data)
if err != nil {
return calendar.Layer{}, err
}
layer := calendar.Layer{Cels: map[string]calendar.RawCelebration{}}
for _, s := range secs {
fields := map[string]string{}
for _, p := range s.Pairs {
fields[p.Key] = p.Val
}
switch {
case s.Name == "" && len(fields) == 0:
continue
case s.Name == "layer":
layer.ID, layer.Name, layer.Type = fields["id"], fields["name"], fields["type"]
default:
base, variant, isVariant := cutVariant(s.Name)
rc := layer.Cels[base]
if rc.Fields == nil {
rc = calendar.RawCelebration{Fields: map[string]string{}, Variants: map[string]map[string]string{}}
}
if isVariant {
rc.Variants[variant] = fields
} else {
for k, v := range fields {
rc.Fields[k] = v
}
}
layer.Cels[base] = rc
}
}
return layer, nil
}
func cutVariant(name string) (base, variant string, ok bool) {
for i := 0; i < len(name); i++ {
if name[i] == '/' {
return name[:i], name[i+1:], true
}
}
return name, "", false
}
|