aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/bible/booktable.go1
-rw-r--r--internal/bible/convert.go1
-rw-r--r--internal/caldata/caldata.go33
-rw-r--r--internal/caldata/tridentine-lectionary.ini222
-rw-r--r--internal/calendar/temporal_ef.go12
-rw-r--r--internal/cli/liturgy.go31
-rw-r--r--scripts/genlect.go110
7 files changed, 394 insertions, 16 deletions
diff --git a/internal/bible/booktable.go b/internal/bible/booktable.go
index de8995c..ffa3907 100644
--- a/internal/bible/booktable.go
+++ b/internal/bible/booktable.go
@@ -139,6 +139,7 @@ func (t *BookTable) Books(dialect string) []BookInfo {
// normalizeSigla.
func (t *BookTable) ParseRef(dialect, input string) (string, bool) {
low := strings.ToLower(strings.Join(strings.Fields(input), " "))
+ low = bookDotRe.ReplaceAllString(low, "$1") // drop abbreviation dots ("1 cor. 9" -> "1 cor 9")
if low == "" {
return "", false
}
diff --git a/internal/bible/convert.go b/internal/bible/convert.go
index 6181dbf..e7d5944 100644
--- a/internal/bible/convert.go
+++ b/internal/bible/convert.go
@@ -46,6 +46,7 @@ var (
iRe = regexp.MustCompile(`\s+i\s+`)
dashRe = regexp.MustCompile(`\s*[-–—]\s*`)
verseLetterRe = regexp.MustCompile(`(\d)[a-c]\b`)
+ bookDotRe = regexp.MustCompile(`(\p{L})\.`) // abbreviation dot after a letter ("Cor." -> "Cor")
psalmRestRe = regexp.MustCompile(`(\d+)(?:\s*\((\d+)\))?(.*)$`)
digitsRe = regexp.MustCompile(`\d+`)
)
diff --git a/internal/caldata/caldata.go b/internal/caldata/caldata.go
index 14b3985..92f9327 100644
--- a/internal/caldata/caldata.go
+++ b/internal/caldata/caldata.go
@@ -19,6 +19,39 @@ var romanCalendar []byte
//go:embed tridentine-calendar.ini
var tridentineCalendar []byte
+//go:embed tridentine-lectionary.ini
+var tridentineLectionary []byte
+
+// efTempReadings maps an EF temporal-day slug to its readings (parsed once).
+var efTempReadings = parseLectionary(tridentineLectionary)
+
+func parseLectionary(data []byte) map[string][]calendar.Reading {
+ l, err := ParseLayer(data)
+ if err != nil {
+ panic(fmt.Sprintf("caldata: tridentine-lectionary.ini invalid: %v", err))
+ }
+ out := map[string][]calendar.Reading{}
+ for slug, rc := range l.Cels {
+ var rs []calendar.Reading
+ for _, part := range []string{"first", "psalm", "second", "gospel"} {
+ if c := rc.Fields[part]; c != "" {
+ rs = append(rs, calendar.Reading{Part: part, Citation: c})
+ }
+ }
+ out[slug] = rs
+ }
+ return out
+}
+
+// TemporalReadings returns the temporal-cycle readings for a computed day slug,
+// or nil. Only the EF ("old") temporal lectionary exists so far.
+func TemporalReadings(form, slug string) []calendar.Reading {
+ if form == "old" {
+ return efTempReadings[slug]
+ }
+ return nil
+}
+
// Universal parses the embedded Ordinary Form universal calendar. It panics on a
// malformed embedded file (a build-time bug caught by tests).
func Universal() calendar.Layer {
diff --git a/internal/caldata/tridentine-lectionary.ini b/internal/caldata/tridentine-lectionary.ini
new file mode 100644
index 0000000..d0002ad
--- /dev/null
+++ b/internal/caldata/tridentine-lectionary.ini
@@ -0,0 +1,222 @@
+; EF (1962) temporal lectionary, keyed by lectio's computed temporal-day slug.
+; Generated from missalemeum (Divinum Officium). Epistle (first) + Gospel citations.
+
+[ef-advent-sunday-1]
+first = Rom 13:11-14
+gospel = Luke 21:25-33
+
+[ef-advent-sunday-2]
+first = Rom 15:4-13
+gospel = Matt 11:2-10
+
+[ef-advent-sunday-3]
+first = Phil 4:4-7
+gospel = John 1:19-28
+
+[ef-advent-sunday-4]
+first = 1 Cor. 4:1-5
+gospel = Luke 3:1-6
+
+[ef-christ-the-king]
+first = Col 1:12-20.
+gospel = John 18:33-37
+
+[ef-christmas-sunday-0]
+first = Gal 4:1-7
+gospel = Luke 2:33-40
+
+[ef-easter-sunday]
+first = 1 Cor 5:7-8
+gospel = Mark 16:1-7
+
+[ef-easter-sunday-3]
+first = 1 Pet 2:21-25
+gospel = John 10:11-16
+
+[ef-easter-sunday-4]
+first = 1 Pet 2:11-19
+gospel = John 16:16-22
+
+[ef-easter-sunday-5]
+first = Jas 1:17-21
+gospel = John 16:5-14
+
+[ef-easter-sunday-6]
+first = Jas 1:22-27
+gospel = John 16:23-30
+
+[ef-easter-sunday-7]
+first = 1 Pet 4:7-11.
+gospel = John 15:26-27; 16:1-4.
+
+[ef-lent-sunday-1]
+first = 2 Cor. 6:1-10
+gospel = Matt 4:1-11
+
+[ef-lent-sunday-2]
+first = 1 Thess. 4:1-7
+gospel = Matt 17:1-9
+
+[ef-lent-sunday-3]
+first = Eph 5:1-9
+gospel = Luke 11:14-28
+
+[ef-lent-sunday-4]
+first = Gal 4:22-31
+gospel = John 6:1-15
+
+[ef-low-sunday]
+first = 1 John 5:4-10
+gospel = John 20. 19-31
+
+[ef-palm-sunday]
+first = Phil 2:5-11
+gospel = Matt. 26:36-75; 27:1-60.
+
+[ef-passion-sunday]
+first = Heb 9:11-15.
+gospel = John 8:46-59.
+
+[ef-pentecost]
+first = Acts 2:1-11.
+gospel = John 14:23-31.
+
+[ef-septuagesima-sunday-1]
+first = 1 Cor. 9:24-27; 10:1-5
+gospel = Matt 20:1-16
+
+[ef-septuagesima-sunday-2]
+first = 2 Cor. 11:19-33; 12:1-9
+gospel = Luke 8:4-15
+
+[ef-septuagesima-sunday-3]
+first = 1 Cor. 13:1-13
+gospel = Luke 18:31-43
+
+[ef-time-after-epiphany-sunday-1]
+first = Col 3:12-17
+gospel = Luke 2:42-52
+
+[ef-time-after-epiphany-sunday-2]
+first = Rom 12:6-16
+gospel = John 2:1-11
+
+[ef-time-after-epiphany-sunday-3]
+first = Rom 12:16-21
+gospel = Matt 8:1-13
+
+[ef-time-after-epiphany-sunday-4]
+first = Mal 3:1-4
+gospel = Luke 2:22-32
+
+[ef-time-after-epiphany-sunday-5]
+first = Col 3:12-17
+gospel = Matt 13:24-30
+
+[ef-time-after-pentecost-sunday-10]
+first = 1 Cor. 12:2-11
+gospel = Luke 18:9-14
+
+[ef-time-after-pentecost-sunday-11]
+first = 1 Cor. 15:1-10
+gospel = Mark 7:31-37
+
+[ef-time-after-pentecost-sunday-12]
+first = 2 Cor. 3:4-9
+gospel = Luke 10:23-37
+
+[ef-time-after-pentecost-sunday-13]
+first = Gal 3:16-22
+gospel = Luke 17:11-19
+
+[ef-time-after-pentecost-sunday-14]
+first = Gal 5:16-24
+gospel = Matt 6:24-33
+
+[ef-time-after-pentecost-sunday-15]
+first = Gal 5:25-26; 6:1-10
+gospel = Luke 7:11-16
+
+[ef-time-after-pentecost-sunday-16]
+first = Eph 3:13-21
+gospel = Luke 14:1-11
+
+[ef-time-after-pentecost-sunday-17]
+first = Eph 4:1-6
+gospel = Matt 22:34-46
+
+[ef-time-after-pentecost-sunday-18]
+first = 1 Cor. 1:4-8
+gospel = Matt 9:1-8
+
+[ef-time-after-pentecost-sunday-19]
+first = Eph 4:23-28
+gospel = Matt 22:1-14
+
+[ef-time-after-pentecost-sunday-2]
+first = 1 John 3:13-18.
+gospel = Luke 14:16-24.
+
+[ef-time-after-pentecost-sunday-20]
+first = Eph 5:15-21
+gospel = John 4:46-53
+
+[ef-time-after-pentecost-sunday-21]
+first = Eph 6:10-17
+gospel = Matt 18:23-35
+
+[ef-time-after-pentecost-sunday-22]
+first = Phil 1:6-11
+gospel = Matt 22:15-21
+
+[ef-time-after-pentecost-sunday-23]
+first = Phil 3:17-21; 4:1-3
+gospel = Matt 9:18-26
+
+[ef-time-after-pentecost-sunday-24]
+first = Rom 13:8-10
+gospel = Matt 8:23-27
+
+[ef-time-after-pentecost-sunday-25]
+first = Col 3:12-17
+gospel = Matt 13:24-30
+
+[ef-time-after-pentecost-sunday-26]
+first = 1 Thess. 1:2-10
+gospel = Matt 13:31-35
+
+[ef-time-after-pentecost-sunday-27]
+first = Col 1:9-14
+gospel = Matt 24:15-35
+
+[ef-time-after-pentecost-sunday-3]
+first = 1 Pet. 5:6-11
+gospel = Luke 15:1-10
+
+[ef-time-after-pentecost-sunday-4]
+first = Rom 8:18-23
+gospel = Luke 5:1-11
+
+[ef-time-after-pentecost-sunday-5]
+first = 1 Pet 3:8-15.
+gospel = Matt 5:20-24.
+
+[ef-time-after-pentecost-sunday-6]
+first = Rom 6:3-11.
+gospel = Mark 8:1-9
+
+[ef-time-after-pentecost-sunday-7]
+first = Rom 6:19-23
+gospel = Matt 7:15-21
+
+[ef-time-after-pentecost-sunday-8]
+first = Rom 8:12-17
+gospel = Luke 16:1-9
+
+[ef-time-after-pentecost-sunday-9]
+first = 1 Cor. 10:6-13
+gospel = Luke 19:41-47
+
+[ef-trinity]
+first = Rom 11:33-36.
+gospel = Matt 28:18-20
diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go
index 725aac2..151bd1b 100644
--- a/internal/calendar/temporal_ef.go
+++ b/internal/calendar/temporal_ef.go
@@ -1,6 +1,9 @@
package calendar
-import "time"
+import (
+ "strconv"
+ "time"
+)
// Extraordinary Form (1962) season constants. Advent/Christmas/Lent/Easter are
// shared with the OF; these are EF-specific.
@@ -114,11 +117,12 @@ func temporalEF(date time.Time) temporalDay {
// Sundays vs ferias of the current season.
if sun {
+ week := efWeek(date, season, y, easter)
rank := RankClass2
if season == Advent && sameDay(date, adventStart(y)) {
rank = RankClass1 // 1st Sunday of Advent
}
- return efCel(season, "ef-"+string(season)+"-sunday", col, rank, efWeek(date, season, y, easter))
+ return efCel(season, "ef-"+string(season)+"-sunday-"+strconv.Itoa(week), col, rank, week)
}
rank := RankClass4 // per-annum feria
if season == Advent || season == Lent || season == Passiontide || season == Septuagesima {
@@ -140,6 +144,10 @@ func efWeek(date time.Time, season Season, y int, easter time.Time) int {
case TimeAfterEpiphany:
jan6 := time.Date(y, 1, 6, 0, 0, 0, 0, time.UTC)
return daysBetween(jan6, date)/7 + 1
+ case Septuagesima:
+ return daysBetween(easter.AddDate(0, 0, -63), date)/7 + 1
+ case Easter_:
+ return daysBetween(easter, date)/7 + 1
default:
return 0
}
diff --git a/internal/cli/liturgy.go b/internal/cli/liturgy.go
index 840e9a6..975917a 100644
--- a/internal/cli/liturgy.go
+++ b/internal/cli/liturgy.go
@@ -78,22 +78,25 @@ func printLiturgicalDay(out io.Writer, cfg config.Config, tbl *bible.BookTable,
fmt.Fprintf(out, " also: %s [%s]\n", n, rankLabel(o.Rank))
}
}
- vern := vernacularVersion(cfg)
+ // Readings: the observed celebration's inline propers, else the temporal
+ // lectionary for the day (proper-of-feast overrides the temporal cycle).
+ var readings []calendar.Reading
for _, m := range day.Observed.Masses {
- for _, r := range m.Readings {
- label := r.Part
- if m.Variant != "" {
- label = m.Variant + " " + label
- }
- disp, txt := readingLine(tbl, cfg, vern, r.Citation)
- fmt.Fprintf(out, "\n %s (%s):\n", label, 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)
+ readings = append(readings, m.Readings...)
+ }
+ if len(readings) == 0 {
+ readings = caldata.TemporalReadings(cfg.Selection().Form, day.Observed.Slug)
+ }
+ 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)
}
}
}
diff --git a/scripts/genlect.go b/scripts/genlect.go
new file mode 100644
index 0000000..eec21b6
--- /dev/null
+++ b/scripts/genlect.go
@@ -0,0 +1,110 @@
+//go:build ignore
+
+// genlect generates the EF temporal Sunday lectionary from missalemeum
+// (Divinum Officium data), keyed by lectio's computed temporal-day slug.
+// One-time; requires network. Run from the repo root:
+// go run scripts/genlect.go
+// Writes internal/caldata/tridentine-lectionary.ini.
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/lukaszkasprzak/lectio/internal/caldata"
+ "github.com/lukaszkasprzak/lectio/internal/calendar"
+)
+
+var citeRe = regexp.MustCompile(`\*([^*]+)\*`)
+
+func fetchCitations(date string) (epistle, gospel string) {
+ resp, err := http.Get("https://www.missalemeum.com/en/api/v5/proper/" + date)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+ var data []struct {
+ Sections []struct {
+ ID string `json:"id"`
+ Body [][]string `json:"body"` // [[english, latin]]
+ } `json:"sections"`
+ }
+ if json.NewDecoder(resp.Body).Decode(&data) != nil || len(data) == 0 {
+ return
+ }
+ for _, s := range data[0].Sections {
+ if len(s.Body) == 0 || len(s.Body[0]) == 0 {
+ continue
+ }
+ m := citeRe.FindStringSubmatch(s.Body[0][0]) // english text carries *citation*
+ if m == nil {
+ continue
+ }
+ switch s.ID {
+ case "Lectio":
+ epistle = strings.TrimSpace(m[1])
+ case "Evangelium":
+ gospel = strings.TrimSpace(m[1])
+ }
+ }
+ return
+}
+
+func main() {
+ sel := calendar.DefaultSelection()
+ sel.Form = "old"
+ layers := []calendar.Layer{caldata.Tridentine()}
+ lect := map[string][2]string{} // slug -> {epistle, gospel}
+
+ start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2028, 12, 31, 0, 0, 0, 0, time.UTC)
+ for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
+ if d.Weekday() != time.Sunday {
+ continue
+ }
+ day := calendar.Compute(d, sel, layers)
+ if day.Observed.Layer != "temporal" { // a feast won that Sunday -> its own propers
+ continue
+ }
+ slug := day.Observed.Slug
+ if _, ok := lect[slug]; ok {
+ continue // one instance per slug
+ }
+ ep, gos := fetchCitations(d.Format("2006-01-02"))
+ if ep == "" && gos == "" {
+ continue
+ }
+ lect[slug] = [2]string{ep, gos}
+ fmt.Fprintf(os.Stderr, "%s %-34s ep=%-22s go=%s\n", d.Format("2006-01-02"), slug, ep, gos)
+ }
+
+ slugs := make([]string, 0, len(lect))
+ for s := range lect {
+ slugs = append(slugs, s)
+ }
+ sort.Strings(slugs)
+ var b strings.Builder
+ b.WriteString("; EF (1962) temporal lectionary, keyed by lectio's computed temporal-day slug.\n")
+ b.WriteString("; Generated from missalemeum (Divinum Officium). Epistle (first) + Gospel citations.\n")
+ for _, s := range slugs {
+ v := lect[s]
+ fmt.Fprintf(&b, "\n[%s]\n", s)
+ if v[0] != "" {
+ fmt.Fprintf(&b, "first = %s\n", v[0])
+ }
+ if v[1] != "" {
+ fmt.Fprintf(&b, "gospel = %s\n", v[1])
+ }
+ }
+ if err := os.WriteFile("internal/caldata/tridentine-lectionary.ini", []byte(b.String()), 0o644); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Fprintf(os.Stderr, "wrote %d temporal entries\n", len(lect))
+}