package readings import ( "fmt" "time" "github.com/lukaszkasprzak/lectio/internal/bible" "github.com/lukaszkasprzak/lectio/internal/caldata" "github.com/lukaszkasprzak/lectio/internal/calendar" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/liturgy" "github.com/lukaszkasprzak/lectio/internal/naming" ) // offlineLoad resolves a day's readings entirely from the embedded calendar // engine and lectionary data -- no network. It returns the same source-agnostic // liturgy.Section / liturgy.DayInfo the CLI/TUI/web already render, so the daily // view is unchanged apart from where its data comes from. Citations are // lectio's English-canonical authored form; the render localises each one to // the chosen corpus's Psalter and the user's sigla dialect (see // render.GatherVersion, bible.OFRef). func offlineLoad(cfg config.Config, date string) ([]liturgy.Section, liturgy.DayInfo, error) { d, err := time.Parse("2006-01-02", date) if err != nil { return nil, liturgy.DayInfo{}, fmt.Errorf("bad date %q (want YYYY-MM-DD)", date) } sel := cfg.Selection() dir, _ := config.CalendarsDir() layers, _ := caldata.Stack(sel.Form, dir, cfg.Use) // Stack falls back to embedded data on error day := calendar.Compute(d.UTC(), sel, layers) rs := caldata.Readings(sel, layers, d.UTC(), day) tbl, _ := bible.LoadBookTable(config.UserBooksINI()) // nil on error -> citations shown as authored return sectionsFor(rs, sel.Form, cfg.UILanguage, cfg.SiglaLang(), tbl), dayInfo(cfg, day), nil } // citationForms renders a reading's authored (English) citation into its // display form (the reader's sigla dialect) and its English-canonical lookup // reference. When the book table is missing or cannot parse the citation, the // authored form is used verbatim for both. func citationForms(raw, siglaLang string, tbl *bible.BookTable) (display, ref string) { if tbl == nil { return raw, raw } canonical, ok := tbl.ParseRef("en", raw) if !ok { return raw, raw } return tbl.FormatRef(siglaLang, canonical), canonical } // ofPart maps a computed reading Part to the modern-lectionary section id and // its Polish heading label. The heading is always the Polish label: the render // (render.LocalizeHeading) rewrites it to English for an English UI, mirroring // how the niedziela sections were shaped. var ofPart = map[string]struct{ id, heading string }{ "first": {"pierwsze_czytanie", "1. czytanie"}, "psalm": {"psalm", "Psalm"}, "second": {"drugie_czytanie", "2. czytanie"}, "acclamation": {"aklamacja", "Aklamacja"}, "gospel": {"ewangelia", "Ewangelia"}, } // efPartHeading gives the traditional (1962) section's ID and heading per UI // language; the EF has only an epistle/lesson and a gospel. The label words are // i18n data, like the modern ones. Unlike the OF headings these are not // translated downstream (render.LocalizeHeading only handles modern IDs), so // they are resolved in the target language here. func efPartHeading(part, lang string) (id, heading string) { switch part { case "first": id = "epistola" case "gospel": id = "evangelium" default: return "", "" } heading = i18n.Get(lang).PartLabel[id] if heading == "" { heading = id } return id, heading } // ofPartOrder and efPartOrder are the display orders of each lectionary's // sections. They are the single source of truth for which part IDs exist. // // ofPartOrder's five modern IDs must remain the same *set* as // internal/render/render.go's modernPartOrder, which lists them in a // different, deliberate order (prefix-match determinism for // render.LocalizeHeading, unrelated to display order). Nothing enforces // that agreement mechanically -- internal/readings and internal/render do // not import each other (adding a cross-package test would create a new // dependency edge that does not exist today) -- so if a sixth modern part // is ever added here, add it to modernPartOrder too, by hand. var ( ofPartOrder = []string{"pierwsze_czytanie", "psalm", "drugie_czytanie", "aklamacja", "ewangelia"} efPartOrder = []string{"epistola", "evangelium"} ) // ofEmittedPartOrder is the subset of ofPartOrder the offline engine can // actually produce, in display order. It excludes "aklamacja": // internal/caldata/caldata.go:42 parses only "first", "psalm", "second" and // "gospel" out of the lectionary data, so no computed OF reading ever carries // Part == "acclamation" and ofPart's "aklamacja" mapping above is never // reached. ofPartOrder stays the full five-ID set on purpose -- it also // drives render.LocalizeHeading's *label* matching, where a scraped heading // can still read "Aklamacja" even though this engine's own readings never // produce that section -- so PartIDs, which promises IDs an app can filter // on, needs this narrower list rather than reusing or shrinking ofPartOrder. var ofEmittedPartOrder = []string{"pierwsze_czytanie", "psalm", "drugie_czytanie", "ewangelia"} // PartIDs returns the part IDs the given lectionary can emit, in display order. // lect takes config.Config.Lectionary's values: "new" or "traditional". An // unknown lectionary returns nil. Callers that build per-part UI (the dlectio // app's reading filters) must derive their list from this rather than // hardcoding IDs. func PartIDs(lect string) []string { switch lect { case "new": return append([]string(nil), ofEmittedPartOrder...) case "traditional": return append([]string(nil), efPartOrder...) } return nil } // sectionsFor turns computed readings into render-ready sections, tagging each // with the section id and heading its form expects and rendering its citation // into display (sigla dialect) and lookup (English-canonical) forms. func sectionsFor(rs []calendar.Reading, form, lang, siglaLang string, tbl *bible.BookTable) []liturgy.Section { var out []liturgy.Section for _, r := range rs { if r.Citation == "" { continue } var id, heading string if form == "old" { id, heading = efPartHeading(r.Part, lang) } else if p, ok := ofPart[r.Part]; ok { id, heading = p.id, p.heading } if id == "" { continue // an unknown part carries no section } display, ref := citationForms(r.Citation, siglaLang, tbl) out = append(out, liturgy.Section{ PartID: id, Heading: heading, Citation: display, Ref: ref, }) } return out } // dayInfo builds the header (celebration name, liturgical colour, rank) for // the computed day. Season is left empty: the celebration name already // carries the temporal identity for temporal days, and the header is a // nice-to-have. func dayInfo(cfg config.Config, day calendar.LiturgicalDay) liturgy.DayInfo { return liturgy.DayInfo{ Name: celebrationName(cfg, day.Observed), Colour: string(day.Colour), Rank: string(day.Observed.Rank), } } // celebrationName is the observed celebration's name in the UI language, // resolved by naming.CelebrationName (name. -> English -> Latin -> // humanized slug). An empty result (unnamed feria) omits the header line. func celebrationName(cfg config.Config, c calendar.Celebration) string { return naming.CelebrationName(cfg.UILanguage, c) }