aboutsummaryrefslogtreecommitdiff
path: root/internal/readings/offline.go
blob: be7c43022cd3d1a827d5561653d340f11e28c073 (plain) (blame)
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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.<lang> -> 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)
}