// 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 - 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 } // 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 "/.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 }