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
|
// 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
// 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
}
// Base returns the universal base layer for a form: the 1962 calendar for
// "old" (EF), the General Roman Calendar otherwise (OF).
func Base(form string) calendar.Layer {
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
}
|