package cli import ( "fmt" "io" "strings" "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/render" ) // runLiturgy prints the computed liturgical day for date (default today) using // the offline calendar engine, and exits. It is the engine's demo/validation // surface; the daily-readings paths still use the scraper. func runLiturgy(cfg config.Config, date string, stdout, stderr io.Writer) int { if date == "" { date = today() } d, err := time.Parse("2006-01-02", date) if err != nil { fmt.Fprintf(stderr, "lectio: bad date %q (want YYYY-MM-DD)\n", date) return 2 } dir, _ := config.CalendarsDir() layers, errs := caldata.Stack(cfg.Selection().Form, dir, cfg.Use) for _, e := range errs { fmt.Fprintln(stderr, "lectio: warning:", e) } day := calendar.Compute(d.UTC(), cfg.Selection(), layers) tbl, _ := bible.LoadBookTable(userBooksINI()) readings := dayReadings(cfg.Selection(), layers, d.UTC(), day) printLiturgicalDay(stdout, cfg, tbl, day, readings) return 0 } // dayReadings resolves the day's Mass readings: the observed celebration's // inline propers, else the temporal lectionary for its slug, else -- for an EF // weekday with no proper Mass -- the preceding Sunday's Mass (the 1962 rule // that green-season ferias repeat the Sunday). func dayReadings(sel calendar.Selection, layers []calendar.Layer, date time.Time, day calendar.LiturgicalDay) []calendar.Reading { var rs []calendar.Reading for _, m := range day.Observed.Masses { rs = append(rs, m.Readings...) } if len(rs) > 0 { return rs } if r := caldata.TemporalReadings(sel.Form, day.Observed.Slug); len(r) > 0 { return r } if sel.Form == "old" && day.Observed.Layer == "temporal" && date.Weekday() != time.Sunday { sun := date.AddDate(0, 0, -int(date.Weekday())) // preceding Sunday (Sunday = 0) sd := calendar.Compute(sun, sel, layers) return caldata.TemporalReadings(sel.Form, sd.Observed.Slug) } return nil } // vernacularVersion is the offline corpus used to render reading text: Wujek // for Polish, Douay-Rheims otherwise (bt is scraped, so it is not used here). func vernacularVersion(cfg config.Config) string { if cfg.TraditionalLang == "pl" || cfg.UILanguage == "pl" { return "wuj" } return "drb" } // readingLine resolves an English-authored citation and returns (display, // text): display is the citation re-formatted in the user's configured sigla // dialect (SiglaLang); text is the verses from version (or "" if unresolved). // The reading data is authored in English, so it is parsed in the "en" dialect; // only the display honors the configured dialect's abbreviations. func readingLine(tbl *bible.BookTable, cfg config.Config, version, citation string) (string, string) { engRef, ok := tbl.ParseRef("en", citation) if !ok { return citation, "" // unparseable: show as authored } display := tbl.FormatRef(cfg.SiglaLang(), engRef) vs, _ := bible.Lookup(version, engRef) parts := make([]string, 0, len(vs)) for _, v := range vs { parts = append(parts, strings.TrimSpace(v.Text)) } return display, strings.Join(parts, " ") } func printLiturgicalDay(out io.Writer, cfg config.Config, tbl *bible.BookTable, day calendar.LiturgicalDay, readings []calendar.Reading) { fmt.Fprintf(out, "%s %s\n", day.Date.Format("2006-01-02"), day.Weekday.String()) if day.SundayCycle != "" { // OF carries the reading cycles; EF does not fmt.Fprintf(out, "season: %s (week %d · Sunday cycle %s · weekday cycle %s)\n", day.Season, day.Week, day.SundayCycle, day.WeekdayCycle) } else { fmt.Fprintf(out, "season: %s (week %d)\n", day.Season, day.Week) } fmt.Fprintf(out, "observed: %s [%s · %s]\n", celebrationName(cfg, day.Observed), rankLabel(day.Observed.Rank), day.Colour) for _, o := range day.Others { if n := celebrationName(cfg, o); n != "" { fmt.Fprintf(out, " also: %s [%s]\n", n, rankLabel(o.Rank)) } } vern := vernacularVersion(cfg) for _, r := range readings { disp, txt := readingLine(tbl, cfg, vern, r.Citation) fmt.Fprintf(out, "\n %s (%s):\n", r.Part, disp) if txt != "" { for _, line := range strings.Split(render.Wrap(txt, 72), "\n") { fmt.Fprintf(out, " %s\n", line) } } else { fmt.Fprintf(out, " (text not in %s)\n", vern) } } } // celebrationName is the celebration's name in the UI language (falling back to // English, then a humanized slug for temporal days that carry no proper name). func celebrationName(cfg config.Config, c calendar.Celebration) string { lang := "en" if cfg.UILanguage == "pl" { lang = "pl" } if n := c.Name[lang]; n != "" { return n } if n := c.Name["en"]; n != "" { return n } if c.Slug == "" { return "(feria)" } return strings.ReplaceAll(strings.TrimPrefix(c.Slug, "ef-"), "-", " ") } func rankLabel(r calendar.Rank) string { switch r { case calendar.RankSolemnity: return "solemnity" case calendar.RankFeast: return "feast" case calendar.RankMemorial: return "memorial" case calendar.RankOptional: return "optional memorial" case calendar.RankClass1: return "I class" case calendar.RankClass2: return "II class" case calendar.RankClass3: return "III class" case calendar.RankClass4: return "IV class" case calendar.RankCommemoration: return "commemoration" default: return "feria" } }