aboutsummaryrefslogtreecommitdiff
path: root/internal/readings
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 12:52:13 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 12:52:13 +0200
commit44472bbe31475a3c672ae003d928d7caffb4b50a (patch)
treee74bae03c61b62479ef4f68b046e3345618933c8 /internal/readings
parent626f2275438a37e5cd2efb5964281b9bef5ce8cc (diff)
parent7b220084cf3951c8cde0582efdfcf628afc64336 (diff)
downloadlectio-44472bbe31475a3c672ae003d928d7caffb4b50a.tar.gz
lectio-44472bbe31475a3c672ae003d928d7caffb4b50a.zip
Merge branch 'of-offline-migration': offline daily view, remove scrapers
Diffstat (limited to 'internal/readings')
-rw-r--r--internal/readings/offline.go140
-rw-r--r--internal/readings/readings.go44
-rw-r--r--internal/readings/readings_test.go82
3 files changed, 171 insertions, 95 deletions
diff --git a/internal/readings/offline.go b/internal/readings/offline.go
new file mode 100644
index 0000000..566fc7a
--- /dev/null
+++ b/internal/readings/offline.go
@@ -0,0 +1,140 @@
+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/liturgy"
+)
+
+// 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 heading per UI language;
+// the EF has only an epistle/lesson and a gospel. Unlike the OF headings these
+// are not translated downstream, so they are set in the target language here.
+func efPartHeading(part, lang string) (id, heading string) {
+ pl := lang == "pl"
+ switch part {
+ case "first":
+ if pl {
+ return "epistola", "Lekcja"
+ }
+ return "epistola", "Lesson"
+ case "gospel":
+ if pl {
+ return "evangelium", "Ewangelia"
+ }
+ return "evangelium", "Gospel"
+ }
+ return "", ""
+}
+
+// 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) 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),
+ }
+}
+
+// celebrationName is the observed celebration's name in the UI language,
+// falling back to English then a humanized slug for temporal days that carry no
+// proper name (mirrors the CLI's cli.celebrationName). An empty result omits
+// the header line.
+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 ""
+ }
+ return calendar.HumanizeSlug(c.Slug)
+}
diff --git a/internal/readings/readings.go b/internal/readings/readings.go
index 49c9adc..d0d7bf9 100644
--- a/internal/readings/readings.go
+++ b/internal/readings/readings.go
@@ -1,7 +1,8 @@
-// Package readings is the single entry point the CLI and TUI use to fetch
-// a day's readings. It routes between the modern (liturgy) and traditional
-// (tradlit) lectionaries by config, then applies part filtering, returning
-// source-agnostic liturgy.Section values either way.
+// Package readings is the single entry point the CLI and TUI use to resolve a
+// day's readings. It computes them offline from the embedded calendar engine
+// and lectionary data (Ordinary Form when cfg.Lectionary is "new", the 1962
+// Extraordinary Form when "traditional"), then applies part filtering, returning
+// source-agnostic liturgy.Section values.
package readings
import (
@@ -9,51 +10,26 @@ import (
"github.com/lukaszkasprzak/lectio/internal/config"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
- "github.com/lukaszkasprzak/lectio/internal/tradlit"
)
// Options controls how Load resolves a day's readings.
type Options struct {
// Date is the day to load, formatted YYYY-MM-DD.
Date string
- // Refresh bypasses cache layers and re-fetches from the network
- // (modern lectionary only; see liturgy.Options.Refresh).
- Refresh bool
- // Offline restricts Load to previously cached/harvested data, never
- // hitting the network (both lectionaries; see liturgy.Options.Offline
- // and tradlit.Load's offline parameter).
- Offline bool
// All, when true, keeps every part the config doesn't explicitly hide;
// when false, only the gospel is kept.
All bool
}
-// Load fetches the day's sections and its DayInfo (celebration name,
-// temporal, liturgical colour -- see liturgy.DayInfo) for the configured
-// lectionary (cfg.Lectionary: "traditional" or "new") and applies part
-// filtering. Returns liturgy.Section values regardless of source. A source
-// that yields no DayInfo (see liturgy.Load/tradlit.Load) comes back as a
-// zero liturgy.DayInfo, not an error -- callers omit the header for it.
+// Load computes the day's sections and its DayInfo (celebration name,
+// liturgical colour -- see liturgy.DayInfo) for the configured form
+// (cfg.Lectionary: "traditional" or "new") and applies part filtering. Every
+// reading is resolved offline from the embedded calendar and lectionary data.
func Load(cfg config.Config, opts Options) ([]liturgy.Section, liturgy.DayInfo, error) {
- offline := opts.Offline || cfg.Offline
-
- var secs []liturgy.Section
- var info liturgy.DayInfo
- var err error
-
- if cfg.Lectionary == "traditional" {
- secs, info, err = tradlit.Load(opts.Date, cfg.TraditionalLang, offline)
- } else {
- secs, info, err = liturgy.Load(liturgy.Options{
- Date: opts.Date,
- Refresh: opts.Refresh,
- Offline: offline,
- })
- }
+ secs, info, err := offlineLoad(cfg, opts.Date)
if err != nil {
return nil, liturgy.DayInfo{}, err
}
-
return filterParts(secs, cfg, opts.All), info, nil
}
diff --git a/internal/readings/readings_test.go b/internal/readings/readings_test.go
index c9e92cd..143b43e 100644
--- a/internal/readings/readings_test.go
+++ b/internal/readings/readings_test.go
@@ -1,10 +1,6 @@
package readings
import (
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
"strings"
"testing"
@@ -79,18 +75,10 @@ func TestFilterPartsTraditionalAllReadingsOnly(t *testing.T) {
}
}
-func TestLoadModernRoutes(t *testing.T) {
- html, err := os.ReadFile("../liturgy/testdata/2026-07-22.html")
- if err != nil {
- t.Fatal(err)
- }
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Write(html)
- }))
- defer srv.Close()
- liturgy.SetBaseURL(srv.URL + "/liturgia/%s/Ewangelia")
- t.Setenv("XDG_CACHE_HOME", t.TempDir())
-
+// TestLoadModern computes the Ordinary Form day offline: the gospel section is
+// present and the header carries the celebration name (English by default,
+// since the universal sanctoral is authored in English).
+func TestLoadModern(t *testing.T) {
cfg := config.Config{Lectionary: "new"}
secs, info, err := Load(cfg, Options{Date: "2026-07-22", All: true})
if err != nil {
@@ -106,57 +94,29 @@ func TestLoadModernRoutes(t *testing.T) {
if !found {
t.Errorf("no gospel section (PartID=ewangelia) found in %+v", secs)
}
- if !strings.Contains(info.Name, "Marii Magdaleny") {
- t.Errorf("DayInfo.Name = %q, want it to contain %q", info.Name, "Marii Magdaleny")
+ if !strings.Contains(info.Name, "Mary Magdalene") {
+ t.Errorf("DayInfo.Name = %q, want it to contain %q", info.Name, "Mary Magdalene")
}
}
-// TestLoadTraditionalOfflineErrorsWithoutCache exercises the (formerly
-// unsupported) traditional+offline path when nothing has been cached yet
-// for that date/lang: it must fail clearly rather than silently falling
-// back to the network or to the modern lectionary's sigla store.
-func TestLoadTraditionalOfflineErrorsWithoutCache(t *testing.T) {
- t.Setenv("XDG_CACHE_HOME", t.TempDir())
-
- cfg := config.Config{Lectionary: "traditional", TraditionalLang: "pl"}
- _, _, err := Load(cfg, Options{Date: "2026-07-22", Offline: true})
- if err == nil {
- t.Fatal("expected error, got nil")
- }
- msg := strings.ToLower(err.Error())
- if !strings.Contains(msg, "no cached") {
- t.Errorf("error %q should mention no cached propers", err.Error())
- }
-}
-
-// TestLoadTraditionalOfflineReadsCache is the positive counterpart: once a
-// prior online Load (or 'lectio update') has cached a date's traditional
-// propers, Load(offline=true) must serve them from disk, no network
-// involved.
-func TestLoadTraditionalOfflineReadsCache(t *testing.T) {
- dir := t.TempDir()
- t.Setenv("XDG_CACHE_HOME", dir)
- cacheDir := filepath.Join(dir, "lectio")
- if err := os.MkdirAll(cacheDir, 0o755); err != nil {
- t.Fatal(err)
- }
- body, err := os.ReadFile("../tradlit/testdata/2026-07-22.json")
- if err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(cacheDir, "2026-07-22.trad.pl.json"), body, 0o644); err != nil {
- t.Fatal(err)
- }
-
- cfg := config.Config{Lectionary: "traditional", TraditionalLang: "pl"}
- secs, info, err := Load(cfg, Options{Date: "2026-07-22", Offline: true, All: true})
+// TestLoadTraditional computes the Extraordinary Form day offline: it never
+// needs the network, and yields the EF epistle+gospel with a header name.
+func TestLoadTraditional(t *testing.T) {
+ cfg := config.Config{Lectionary: "traditional"}
+ secs, info, err := Load(cfg, Options{Date: "2026-07-22", All: true})
if err != nil {
t.Fatalf("Load: %v", err)
}
- if len(secs) == 0 {
- t.Error("expected traditional offline sections, got none")
+ found := false
+ for _, s := range secs {
+ if s.PartID == "evangelium" {
+ found = true
+ }
+ }
+ if !found {
+ t.Errorf("no EF gospel section (PartID=evangelium) found in %+v", secs)
}
- if info.Name != "St. Mary Magdalene" {
- t.Errorf("DayInfo.Name = %q, want %q", info.Name, "St. Mary Magdalene")
+ if info.Name == "" {
+ t.Error("DayInfo.Name empty for a traditional feast day")
}
}