aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/liturgy.go
blob: 9d7f3b6f238125541b47d6c73e638b3a1d50db6a (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
175
176
177
178
179
180
181
182
183
184
185
186
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/i18n"
	"github.com/lukaszkasprzak/lectio/internal/naming"
	"github.com/lukaszkasprzak/lectio/internal/render"
)

// runLiturgy prints the computed liturgical day for date (default today) using
// the offline calendar engine, and exits. It shows the day's identity and
// resolved reading citations; the daily-readings paths render the same offline
// engine's readings as full text.
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 logic lives in
// caldata.Readings (shared with calfeed.Build) so it stays reusable outside
// the CLI.
func dayReadings(sel calendar.Selection, layers []calendar.Layer, date time.Time, day calendar.LiturgicalDay) []calendar.Reading {
	return caldata.Readings(sel, layers, date, day)
}

// vernacularVersion is the corpus that renders reading text: the config's
// resolved reading corpus (Config.ReadingCorpus -- an explicit
// reading_version, else a reading_lang/ui_language match against
// autoselect-eligible corpora), else the complete Latin Vulgate. The
// built-in Wujek (wuj) sets autoselect=false in its sidecar (it lacks the
// deuterocanonical Daniel/Esther additions and ~400 verses its builder
// dropped), so Polish still falls back to Latin until a properly-formatted
// Polish Bible is supplied as a user corpus (the national-bibles feature);
// wuj stays available for explicit `--ref -b wuj` and version comparison.
func vernacularVersion(cfg config.Config) string {
	if code := cfg.ReadingCorpus(); code != "" {
		return code
	}
	return latinFallback // "vul"
}

// versionDisplayName resolves a corpus code's display label for lang: the
// localized i18n chrome label (internal/i18n) when lang defines one for
// code, else the corpus's own sidecar Name (bible.Meta -- covers user and
// other registered corpora the built-in i18n tables don't know about), else
// the bare code.
func versionDisplayName(lang, code string) string {
	if l, ok := i18n.Get(lang).Version[code]; ok {
		return l
	}
	if m, ok := bible.Meta(code); ok && m.Name != "" {
		return m.Name
	}
	return code
}

// latinFallback is the corpus used to render a passage the vernacular corpus
// lacks: the Latin Vulgate, which carries the full canon -- including the
// deuterocanonical Daniel (13-14) and Esther (11-16) additions the vernacular
// corpora omit and which the EF Lenten lectionary reads. The EF is a Latin
// rite, so the Latin is the authoritative text to fall back to.
const latinFallback = "vul"

// readingLine resolves an English-authored citation and returns (display, text,
// used): display is the citation re-formatted in the user's configured sigla
// dialect (SiglaLang); text is the verses; used is the version that actually
// supplied the text -- version, else latinFallback, else "" when neither has
// it. 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) (display, text, used string) {
	engRef, ok := tbl.ParseRef("en", citation)
	if !ok {
		return citation, "", "" // unparseable: show as authored
	}
	display = tbl.FormatRef(cfg.SiglaLang(), engRef)
	if txt := lookupText(version, engRef); txt != "" {
		return display, txt, version
	}
	if version != latinFallback {
		if txt := lookupText(latinFallback, engRef); txt != "" {
			return display, txt, latinFallback
		}
	}
	return display, "", ""
}

// lookupText joins the verses of ref in version into one string ("" if none).
func lookupText(version, ref string) string {
	vs, _ := bible.Lookup(version, ref)
	parts := make([]string, 0, len(vs))
	for _, v := range vs {
		parts = append(parts, strings.TrimSpace(v.Text))
	}
	return 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, used := readingLine(tbl, cfg, vern, r.Citation)
		fmt.Fprintf(out, "\n  %s (%s):\n", r.Part, disp)
		if txt == "" {
			fmt.Fprintf(out, "    (text not in %s)\n", vern)
			continue
		}
		if used != vern { // the vernacular lacked it; fell back to Latin
			fmt.Fprintf(out, "    [%s has no text here -- showing %s]\n",
				versionDisplayName("en", vern), versionDisplayName("en", used))
		}
		for _, line := range strings.Split(render.Wrap(txt, 72), "\n") {
			fmt.Fprintf(out, "    %s\n", line)
		}
	}
}

// celebrationName is the celebration's name in the UI language, resolved by
// naming.CelebrationName (name.<lang> -> English -> Latin -> humanized slug).
// A bare, unnamed feria renders as "(feria)".
func celebrationName(cfg config.Config, c calendar.Celebration) string {
	if n := naming.CelebrationName(cfg.UILanguage, c); n != "" {
		return n
	}
	return "(feria)"
}

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