summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 09:46:54 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 09:46:54 +0200
commit97f704b659ea6c72c4aa354378b347283ecc5cac (patch)
tree2606730c373ba97ebec6bcf3b38ebb00f13c0a29
parent6641ce335de522e6d51c47ad9d14f69726650592 (diff)
downloadlectio-97f704b659ea6c72c4aa354378b347283ecc5cac.tar.gz
lectio-97f704b659ea6c72c4aa354378b347283ecc5cac.zip
feat(of): proper Mass readings for the sanctoral (from catholic-resources.org)
OF feasts, solemnities, and proper-reading memorials had no readings of their own and fell back to the day's ferial (Barnabas showed the ferial instead of Acts 11; Our Lady of Sorrows / the Guardian Angels showed the ferial instead of their proper gospel). scripts/gen-of-sanctoral-readings.py scrapes catholic-resources.org's Proper of Saints table and inlines reading.first/psalm/second/gospel into roman-calendar.ini for the 41 universal-calendar celebrations marked PROPER (plus every feast/solemnity); memorials that draw on a Common keep the ferial, which is the OF default. Handles the source quirks: 'or' alternatives (takes the first), cycle-specific A:/B:/C: gospels (takes A), lectionary # refs, and vigil-vs-day (keeps the day Mass). All 41 render; citations only. Adds TestUniversalSanctoralReadings, credits the source in NOTICE, bumps to 0.41.0.
-rw-r--r--NOTICE8
-rw-r--r--internal/caldata/caldata_test.go26
-rw-r--r--internal/caldata/roman-calendar.ini134
-rw-r--r--internal/config/config.go2
-rw-r--r--scripts/gen-of-sanctoral-readings.py136
5 files changed, 302 insertions, 4 deletions
diff --git a/NOTICE b/NOTICE
index a593ea4..6659847 100644
--- a/NOTICE
+++ b/NOTICE
@@ -27,9 +27,11 @@ internal/caldata/tridentine-lectionary.ini (Extraordinary Form) and
internal/caldata/of-lectionary.ini (Ordinary Form) were compiled from
missalemeum.com (Divinum Officium data, EF), niedziela.pl (OF Sundays), and
catholic-resources.org (Fr. Felix Just, S.J. — the Ordinary Form weekday/ferial
-lectionary of Ordinary Time, scripts/genlect-of-cr.py). Only the references were
-taken; the reading TEXT is rendered from the public-domain corpora below — no
-copyrighted translation is redistributed.
+lectionary of Ordinary Time via scripts/genlect-of-cr.py, and the proper readings
+of the OF sanctoral — feasts, solemnities and proper-reading memorials —
+inlined into roman-calendar.ini via scripts/gen-of-sanctoral-readings.py). Only
+the references were taken; the reading TEXT is rendered from the public-domain
+corpora below — no copyrighted translation is redistributed.
OF sanctoral calendar
---------------------
diff --git a/internal/caldata/caldata_test.go b/internal/caldata/caldata_test.go
index 5ffa57e..0d6616e 100644
--- a/internal/caldata/caldata_test.go
+++ b/internal/caldata/caldata_test.go
@@ -1,6 +1,7 @@
package caldata
import (
+ "strings"
"testing"
"github.com/lukaszkasprzak/lectio/internal/calendar"
@@ -30,6 +31,31 @@ func TestUniversalLoads(t *testing.T) {
}
}
+func TestUniversalSanctoralReadings(t *testing.T) {
+ l := Universal()
+ // A feast/solemnity or a PROPER-reading memorial carries its own inline Mass
+ // (imported from catholic-resources.org) rather than falling back to the ferial.
+ want := map[string][2]string{ // slug -> {reading.first substr, reading.gospel substr}
+ "barnabas-the-apostle": {"Acts 11", "Matthew 10:7-13"},
+ "assumption-of-the-blessed-virgin-mary": {"Revelation 11", "Luke 1:39-56"},
+ "our-lady-of-sorrows": {"Hebrews 5", "John 19:25-27"},
+ "all-saints": {"Revelation 7", "Matthew 5:1-12"},
+ }
+ for slug, w := range want {
+ rc, ok := l.Cels[slug]
+ if !ok {
+ t.Errorf("missing %q", slug)
+ continue
+ }
+ if got := rc.Fields["reading.first"]; !strings.Contains(got, w[0]) {
+ t.Errorf("%s: reading.first = %q, want containing %q", slug, got, w[0])
+ }
+ if got := rc.Fields["reading.gospel"]; !strings.Contains(got, w[1]) {
+ t.Errorf("%s: reading.gospel = %q, want containing %q", slug, got, w[1])
+ }
+ }
+}
+
func TestTridentineLoads(t *testing.T) {
l := Tridentine()
if l.ID != "tridentine" {
diff --git a/internal/caldata/roman-calendar.ini b/internal/caldata/roman-calendar.ini
index 63fd703..ccffa64 100644
--- a/internal/caldata/roman-calendar.ini
+++ b/internal/caldata/roman-calendar.ini
@@ -35,6 +35,9 @@ class = lord
colour = white
name.en = The Most Holy Name of Jesus
name.la = Ss.mi Nominis Iesu
+reading.first = Philippians 2:1-11
+reading.psalm = Psalms 8:4-5,6-7,8-9
+reading.gospel = Luke 2:21-24
[raymond-of-penyafort-priest]
date = 01-07
@@ -111,6 +114,9 @@ colour = white
name.en = The Conversion of Saint Paul, apostle
name.pl = Nawrócenia św. Pawła Apostoła
name.la = In conversione S. Pauli, apostoli
+reading.first = The Acts 22:3-16
+reading.psalm = Psalms 117:1bc,2
+reading.gospel = Mark 16:15-18
[saints-timothy-and-titus-bishops]
date = 01-26
@@ -119,6 +125,9 @@ class = saint
colour = white
name.en = Saints Timothy and Titus, bishops
name.la = Ss. Timothei et Titi, episcoporum
+reading.first = 2 Timothy 1:1-8
+reading.psalm = Psalms 96:1-2a,2b-3,7-8a,10
+reading.gospel = Luke 10:1-9
[angela-merici-virgin]
date = 01-27
@@ -153,6 +162,10 @@ class = lord
colour = white
name.en = Presentation of the Lord
name.la = IN PRAESENTATIONE DOMINI
+reading.first = Malachi 3:1-4
+reading.psalm = Psalms 24:7,8,9,10
+reading.second = Hebrews 2:14-18
+reading.gospel = Luke 2:22-40
[ansgar-bishop]
date = 02-03
@@ -254,6 +267,9 @@ colour = white
name.en = Chair of Saint Peter, apostle
name.pl = katedry św. Piotra Apostoła
name.la = Cathedrae S. Petri, apostoli
+reading.first = 1 Peter 5:1-4
+reading.psalm = Psalms 23:1-3a,4,5,6
+reading.gospel = Matthew 16:13-19
[polycarp-bishop-and-martyr]
date = 02-23
@@ -319,6 +335,10 @@ colour = white
name.en = Saint Joseph Husband of the Blessed Virgin Mary
name.pl = św. Józefa, Oblubieńca Najświętszej Maryi Panny
name.la = S. IOSEPH, SPONSI B. M. V.
+reading.first = 2 Samuel 7:4-5a,12-14a,16
+reading.psalm = Psalms 89:2-3,4-5,27+29
+reading.second = Romans 4:13,16-18,22
+reading.gospel = Matthew 1:16,18-21,24a
[turibius-of-mogrovejo-bishop]
date = 03-23
@@ -336,6 +356,10 @@ colour = white
name.en = Annunciation of the Lord
name.pl = Zwiastowania Pańskiego
name.la = IN ANNUNTIATIONE DOMINI
+reading.first = Isaiah 7:10-14;8:10
+reading.psalm = Psalms 40:7-8a,8b-9,10,11
+reading.second = Hebrews 10:4-10
+reading.gospel = Luke 1:26-38
[francis-of-paola-hermit]
date = 04-02
@@ -424,6 +448,9 @@ class = saint
colour = red
name.en = Saint Mark the Evangelist
name.la = S. Marci, evangelistae
+reading.first = 1 Peter 5:5b-14
+reading.psalm = Psalms 89:2-3,6-7,16-17
+reading.gospel = Mark 16:15-20
[louis-grignon-de-montfort-priest]
date = 04-28
@@ -465,6 +492,9 @@ class = saint
colour = white
name.en = Saint Joseph the Worker
name.la = S. Ioseph Opificis
+reading.first = Genesis 1:26-2:3
+reading.psalm = Psalms 90:2,3-4,12-13,14+16
+reading.gospel = Matthew 13:54-58
[athanasius-bishop-and-doctor]
date = 05-02
@@ -483,6 +513,9 @@ colour = red
name.en = Saints Philip and James, Apostles
name.pl = Najświętszej Maryi Panny, Królowej Polski
name.la = Ss. Philippi et Iacobi, apostolorum
+reading.first = 1 Corinthians 15:1-8
+reading.psalm = Psalms 19:2-3,4-5
+reading.gospel = John 14:6-14
[saints-nereus-and-achilleus-martyrs]
date = 05-12
@@ -516,6 +549,9 @@ colour = red
name.en = Saint Matthias the Apostle
name.pl = św. Macieja, apostoła
name.la = S. Matthiae, apostoli
+reading.first = The Acts 1:15-17,20-26
+reading.psalm = Psalms 113:1-2,3-4,5-6,7-8
+reading.gospel = John 15:9-17
[john-i-pope-and-martyr]
date = 05-18
@@ -598,6 +634,9 @@ colour = white
name.en = Visitation of the Blessed Virgin Mary
name.pl = Nawiedzenia Najświętszej Maryi Panny
name.la = In Visitatione B. Mariae Virginis
+reading.first = Zephaniah 3:14-18a
+reading.psalm = Isaiah 12:2-3,4bcd,5-6
+reading.gospel = Luke 1:39-56
[justin-martyr]
date = 06-01
@@ -657,6 +696,9 @@ colour = red
name.en = Saint Barnabas the Apostle
name.pl = św. Barnaby, apostoła
name.la = S. Barnabae, apostoli
+reading.first = The Acts 11:21b-26;13:1-3
+reading.psalm = Psalms 98:1,2-3ab,3cd-4,5-6
+reading.gospel = Matthew 10:7-13
[anthony-of-padua-priest-and-doctor]
date = 06-13
@@ -708,6 +750,10 @@ colour = white
name.en = Birth of Saint John the Baptist
name.pl = narodzenia św. Jana Chrzciciela
name.la = IN NATIVITATE S. IOANNIS BAPTISTAE
+reading.first = Isaiah 49:1-6
+reading.psalm = Psalms 139:1b-3,13-14ab,14c-15
+reading.second = The Acts 13:22-26
+reading.gospel = Luke 1:57-66,80
[cyril-of-alexandria-bishop-and-doctor]
date = 06-27
@@ -732,6 +778,10 @@ class = saint
colour = red
name.en = Saints Peter and Paul, Apostles
name.la = Ss. PETRI ET PAULI, APOSTOLORUM
+reading.first = The Acts 12:1-11
+reading.psalm = Psalms 34:2-3,4-5,6-7,8-9
+reading.second = 2 Timothy 4:6-8,17-18
+reading.gospel = Matthew 16:13-19
[first-martyrs-of-the-church-of-rome]
date = 06-30
@@ -749,6 +799,9 @@ colour = red
name.en = Saint Thomas the Apostle
name.pl = św. Tomasza, apostoła
name.la = S. Thomae, apostoli
+reading.first = Ephesians 2:19-22
+reading.psalm = Psalms 117:1bc,2
+reading.gospel = John 20:24-29
[elizabeth-of-portugal]
date = 07-04
@@ -848,6 +901,9 @@ colour = white
name.en = Saint Mary Magdalene
name.pl = św. Marii Magdaleny
name.la = S. Mariae Magdalenae
+reading.first = Song of Solomon 3:1-4b
+reading.psalm = Psalms 63:2,3-4,5-6,8-9
+reading.gospel = John 20:1-2,11-18
[birgitta-religious]
date = 07-23
@@ -873,6 +929,9 @@ colour = red
name.en = Saint James, apostle
name.pl = św. Jakuba, apostoła
name.la = S. Iacobi, apostoli
+reading.first = 2 Corinthians 4:7-15
+reading.psalm = Psalms 126:1bc-2ab,2cd-3,4-5,6
+reading.gospel = Matthew 20:20-28
[saints-joachim-and-anne]
date = 07-26
@@ -882,6 +941,9 @@ colour = white
name.en = Saints Joachim and Anne
name.pl = św. Joachima i Anny, rodziców Najświętszej Maryi Panny
name.la = Ss. Ioachim et Annae, parentum B. Mariae Virginis
+reading.first = Sirach 44:1,10-15
+reading.psalm = Psalms 132:11,13-14,17-18
+reading.gospel = Matthew 13:16-17
[martha]
date = 07-29
@@ -891,6 +953,9 @@ colour = red
name.en = Saint Martha
name.pl = św. Marty, Marii i Łazarza
name.la = S. Marthae
+reading.first = 1 John 4:7-16
+reading.psalm = Psalms 34:2-3,4-5,6-7,8-9,10-11
+reading.gospel = John 11:19-27
[peter-chrysologus-bishop-and-doctor]
date = 07-30
@@ -959,6 +1024,10 @@ colour = white
name.en = Transfiguration of the Lord
name.pl = Przemienienia Pańskiego
name.la = IN TRANSFIGURATIONE DOMINI
+reading.first = Daniel 7:9-10,13-14
+reading.psalm = Psalms 97:1-2,5-6,9
+reading.second = 2 Peter 1:16-19
+reading.gospel = Matthew 17:1-9
[sixtus-ii-pope-and-companions-martyrs]
date = 08-07
@@ -1000,6 +1069,9 @@ class = saint
colour = red
name.en = Saint Lawrence, deacon and martyr
name.la = S. Laurentii, diaconi et martyris
+reading.first = 2 Corinthians 9:6-10
+reading.psalm = Psalms 112:1-2,5-6,7-8,9
+reading.gospel = John 12:24-26
[clare-virgin]
date = 08-11
@@ -1043,6 +1115,10 @@ colour = white
name.en = Assumption of the Blessed Virgin Mary
name.pl = Wniebowzięcia Najświętszej Maryi Panny
name.la = IN ASSUMPTIONE B. MARIAE VIRGINIS
+reading.first = Revelation 11:19a;12:1-6a,10ab
+reading.psalm = Psalms 45:10,11,12,16
+reading.second = 1 Corinthians 15:20-27
+reading.gospel = Luke 1:39-56
[stephen-of-hungary]
date = 08-16
@@ -1102,6 +1178,9 @@ class = saint
colour = red
name.en = Saint Bartholomew the Apostle
name.la = S. Bartholomaei, apostoli
+reading.first = Revelation 21:9b-14
+reading.psalm = Psalms 145:10-11,12-13,17-18
+reading.gospel = John 1:45-51
[joseph-of-calasanz-priest]
date = 08-25
@@ -1145,6 +1224,9 @@ colour = red
name.en = The Beheading of Saint John the Baptist, martyr
name.pl = męczeństwa św. Jana Chrzciciela
name.la = In Passione S. Ioannis Baptistae, martyris
+reading.first = Jeremiah 1:17-19
+reading.psalm = Psalms 71:1-2,3-4a,5-6ab,15ab+17
+reading.gospel = Mark 6:17-29
[gregory-the-great-pope-and-doctor]
date = 09-03
@@ -1163,6 +1245,9 @@ colour = white
name.en = Birth of the Blessed Virgin Mary
name.pl = Narodzenia Najświętszej Maryi Panny
name.la = In Nativitate B. Mariae Virginis
+reading.first = Micah 5:1-4a
+reading.psalm = Psalms 13:6ab,6c
+reading.gospel = Matthew 1:1-16,18-23
[peter-claver-priest]
date = 09-09
@@ -1196,6 +1281,10 @@ class = lord
colour = red
name.en = Triumph of the Holy Cross
name.la = IN EXALTATIONE SANCTAE CRUCIS
+reading.first = Numbers 21:4b-9
+reading.psalm = Psalms 78:1bc-2,34-35,36-37,38
+reading.second = Philippians 2:6-11
+reading.gospel = John 3:13-17
[our-lady-of-sorrows]
date = 09-15
@@ -1205,6 +1294,9 @@ colour = white
name.en = Our Lady of Sorrows
name.pl = Najświętszej Maryi Panny Bolesnej
name.la = B. Mariae Virginis Perdolentis
+reading.first = Hebrews 5:7-9
+reading.psalm = Psalms 31:2-3b,3cd-4,5-6,15-16,20
+reading.gospel = John 19:25-27
[saints-cornelius-pope-and-cyprian-bishop-martyrs]
date = 09-16
@@ -1247,6 +1339,9 @@ class = saint
colour = red
name.en = Saint Matthew the Evangelist, Apostle, Evangelist
name.la = S. Matthaei, apostoli et evangelistae
+reading.first = Ephesians 4:1-7,11-13
+reading.psalm = Psalms 19:2-3,4-5
+reading.gospel = Matthew 9:9-13
[pio-of-pietrelcina-padre-pio-priest]
date = 09-23
@@ -1298,6 +1393,9 @@ colour = white
name.en = Saints Michael, Gabriel and Raphael, Archangels
name.pl = świętych Archaniołów Michała, Gabriela i Rafała
name.la = Ss. Michaelis, Gabrielis et Raphaelis, archangelorum
+reading.first = Daniel 7:9-10,13-14
+reading.psalm = Psalms 138:1-2ab,2cde-3,4-5
+reading.gospel = John 1:47-51
[jerome-priest-and-doctor]
date = 09-30
@@ -1325,6 +1423,9 @@ colour = white
name.en = Guardian Angels
name.pl = świętych Aniołów Stróżów
name.la = Ss. Angelorum Custodum
+reading.first = Exodus 23:20-23
+reading.psalm = Psalms 91:1-2,3-4ab,4c-6,10-11
+reading.gospel = Matthew 18:1-5,10
[francis-of-assisi]
date = 10-04
@@ -1426,6 +1527,9 @@ colour = red
name.en = Saint Luke the Evangelist
name.pl = św. Łukasza, ewangelisty
name.la = S. Lucae, evangelistae
+reading.first = 2 Timothy 4:10-17b
+reading.psalm = Psalms 145:10-11,12-13,17-18
+reading.gospel = Luke 10:1-9
[paul-of-the-cross-priest]
date = 10-19
@@ -1475,6 +1579,9 @@ colour = red
name.en = Saint Simon and Saint Jude, apostles
name.pl = świętych Apostołów Szymona i Judy Tadeusza
name.la = Ss. Simonis et Iudae, apostolorum
+reading.first = Ephesians 2:19-22
+reading.psalm = Psalms 19:2-3,4-5
+reading.gospel = Luke 6:12-19
[all-saints]
date = 11-01
@@ -1484,6 +1591,10 @@ colour = white
name.en = All Saints
name.pl = Wszystkich Świętych
name.la = OMNIUM SANCTORUM
+reading.first = Revelation 7:2-4,9-14
+reading.psalm = Psalms 24:1bc-2,3-4ab,5-6
+reading.second = 1 John 3:1-3
+reading.gospel = Matthew 5:1-12a
[all-souls]
date = 11-02
@@ -1517,6 +1628,10 @@ class = lord
colour = white
name.en = Dedication of the Lateran basilica
name.la = IN DEDICATIONE BASILICAE LATERANENSIS
+reading.first = Ezekiel 47:1-2,8-9,12
+reading.psalm = Psalms 46:2-3,5-6,8-9
+reading.second = 1 Corinthians 3:9c-11,16-17
+reading.gospel = John 2:13-22
[leo-the-great-pope-and-doctor]
date = 11-10
@@ -1585,6 +1700,9 @@ class = saint
colour = white
name.en = Dedication of the basilicas of Saints Peter and Paul, Apostles
name.la = In Dedicatione basilicarum Ss. Petri et Pauli, apostolorum
+reading.first = The Acts 28:11-16,30-31
+reading.psalm = Psalms 98:1,2-3ab,3cd-4,5-6
+reading.gospel = Matthew 14:22-33
[presentation-of-the-blessed-virgin-mary]
date = 11-21
@@ -1644,6 +1762,9 @@ class = saint
colour = red
name.en = Saint Andrew the Apostle
name.la = S. Andreae, apostoli
+reading.first = Romans 10:9-18
+reading.psalm = Psalms 19:8,9,10,11
+reading.gospel = Matthew 4:18-22
[francis-xavier-priest]
date = 12-03
@@ -1686,6 +1807,10 @@ colour = white
name.en = Immaculate Conception of the Blessed Virgin Mary
name.pl = Niepokalanego Poczęcia Najświętszej Maryi Panny
name.la = IN CONCEPTIONE IMMACULATA BEATAE MARIAE VIRGINIS
+reading.first = Genesis 3:9-15,20
+reading.psalm = Psalms 98:1,2-3ab,3cd-4
+reading.second = Ephesians 1:3-6,11-12
+reading.gospel = Luke 1:26-38
[juan-diego]
date = 12-09
@@ -1752,6 +1877,9 @@ colour = red
name.en = Saint Stephen, the first martyr
name.pl = św. Szczepana, pierwszego męczennika
name.la = S. Stephani, protomartyris
+reading.first = The Acts 6:8-10;7:54-59
+reading.psalm = Psalms 31:3cd-4,6+8ab,16bc+17
+reading.gospel = Matthew 10:17-22
[john-the-apostle-and-evangelist]
date = 12-27
@@ -1761,6 +1889,9 @@ colour = white
name.en = Saint John the Apostle and evangelist
name.pl = św. Jana, apostoła i ewangelisty
name.la = S. Ioannis, apostoli et evangelistae
+reading.first = 1 John 1:1-4
+reading.psalm = Psalms 97:1-2,5-6,11-12
+reading.gospel = John 20:1a+2-8
[holy-innocents-martyrs]
date = 12-28
@@ -1769,6 +1900,9 @@ class = saint
colour = red
name.en = Holy Innocents, martyrs
name.la = Ss. Innocentium, martyrum
+reading.first = 1 John 1:5-2:2
+reading.psalm = Psalms 124:2-3,4-5,7b-8
+reading.gospel = Matthew 2:13-18
[thomas-becket-bishop-and-martyr]
date = 12-29
diff --git a/internal/config/config.go b/internal/config/config.go
index e84eabe..35f5990 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -63,7 +63,7 @@ const configHeader = `# lectio configuration (INI). Full-line comments only (# o
// Version is lectio's release version, shared by every binary's
// -v/--version output (lectio, lectio-ui, lectio-web).
-const Version = "0.40.0"
+const Version = "0.41.0"
// validVersions are the five scripture versions lectio understands.
var validVersions = map[string]bool{
diff --git a/scripts/gen-of-sanctoral-readings.py b/scripts/gen-of-sanctoral-readings.py
new file mode 100644
index 0000000..fd9c035
--- /dev/null
+++ b/scripts/gen-of-sanctoral-readings.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""Add PROPER Mass readings to the Ordinary Form sanctoral
+(internal/caldata/roman-calendar.ini) from catholic-resources.org's Proper of
+Saints table (Fr. Felix Just, S.J.).
+
+Without this, an OF feast/solemnity or a proper-reading memorial (e.g. the
+Apostles' feasts, the Assumption, All Saints, St Barnabas, Our Lady of Sorrows,
+the Guardian Angels) has no readings of its own and falls back to the day's
+ferial. Only celebrations whose readings are marked PROPER (and every feast /
+solemnity) are imported; memorials that draw on a Common keep the ferial, which
+is the OF default. Scripture CITATIONS only; text is rendered from the public-
+domain corpora. See NOTICE.
+
+Requires network. From the repo root: python3 scripts/gen-of-sanctoral-readings.py
+Adds reading.first / reading.psalm / reading.second / reading.gospel lines to the
+matching entries in internal/caldata/roman-calendar.ini.
+"""
+import re, sys, html, urllib.request
+
+INI = "internal/caldata/roman-calendar.ini"
+URL = "https://www.catholic-resources.org/Lectionary/2002USL-Sanctoral.htm"
+MONS = {m: i + 1 for i, m in enumerate("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split())}
+
+# CR abbreviation -> lectio full book name (same table as genlect-of-cr.py).
+BOOK = {}
+def reg(full, *al):
+ for a in al: BOOK[a.lower().replace(".", "").strip()] = full
+reg("Genesis","gen");reg("Exodus","exod","ex");reg("Leviticus","lev");reg("Numbers","num")
+reg("Deuteronomy","deut");reg("Joshua","josh");reg("Judges","judg");reg("Ruth","ruth")
+reg("1 Samuel","1 sam");reg("2 Samuel","2 sam");reg("1 Kings","1 kgs");reg("2 Kings","2 kgs")
+reg("1 Chronicles","1 chr");reg("2 Chronicles","2 chr");reg("Ezra","ezra");reg("Nehemiah","neh")
+reg("Tobit","tob");reg("Judith","jdt");reg("Esther","esth");reg("1 Maccabees","1 macc");reg("2 Maccabees","2 macc")
+reg("Job","job");reg("Psalms","ps","pss");reg("Proverbs","prov");reg("Ecclesiastes","eccl")
+reg("Song of Solomon","song","cant");reg("Wisdom","wis");reg("Sirach","sir")
+reg("Isaiah","isa");reg("Jeremiah","jer");reg("Lamentations","lam");reg("Baruch","bar")
+reg("Ezekiel","ezek");reg("Daniel","dan");reg("Hosea","hos");reg("Joel","joel");reg("Amos","amos")
+reg("Obadiah","obad");reg("Jonah","jonah");reg("Micah","mic","micah");reg("Nahum","nah");reg("Habakkuk","hab")
+reg("Zephaniah","zeph");reg("Haggai","hag");reg("Zechariah","zech");reg("Malachi","mal")
+reg("Matthew","matt","mat");reg("Mark","mark");reg("Luke","luke");reg("John","john")
+reg("The Acts","acts");reg("Romans","rom");reg("1 Corinthians","1 cor");reg("2 Corinthians","2 cor")
+reg("Galatians","gal");reg("Ephesians","eph");reg("Philippians","phil");reg("Colossians","col")
+reg("1 Thessalonians","1 thess");reg("2 Thessalonians","2 thess");reg("1 Timothy","1 tim");reg("2 Timothy","2 tim")
+reg("Titus","titus");reg("Philemon","phlm");reg("Hebrews","heb");reg("James","jas")
+reg("1 Peter","1 pet");reg("2 Peter","2 pet");reg("1 John","1 john");reg("2 John","2 john");reg("3 John","3 john")
+reg("Jude","jude");reg("Revelation","rev","apoc")
+
+def clean_cite(raw):
+ s = html.unescape(raw or "")
+ s = re.sub(r"[–—]", "-", s)
+ s = re.sub(r"\(#[^)]*\)", "", s) # drop lectionary numbers
+ s = re.sub(r"\bo?r\s+elsewhere.*", "", s, flags=re.I)
+ # cycle-specific gospel "A: ... B: ... C: ..." -> take the A option
+ m = re.search(r"\bA:\s*(.+?)(?:\s+B:|$)", s)
+ if m:
+ s = m.group(1)
+ s = re.split(r"\s+or\b", s, maxsplit=1)[0] # first of alternative readings
+ s = s.strip().strip(";,").strip()
+ if not s or s in (".", "x") or s.startswith("[") or s.startswith("("):
+ return ""
+ m = re.match(r"((?:[1-4]\s+)?[A-Za-z][A-Za-z]*\.?)\s*(.*)", s)
+ if not m:
+ return ""
+ tok = m.group(1).lower().replace(".", "").strip()
+ full = BOOK.get(tok)
+ if not full:
+ sys.stderr.write(f" ! unknown book {m.group(1)!r} in {raw!r}\n")
+ return ""
+ verses = re.sub(r",\s+", ",", m.group(2)).replace(" ", "")
+ return f"{full} {verses}".strip()
+
+def strip_tags(s):
+ return html.unescape(re.sub(r"<[^>]+>", "", s)).replace("\xa0", " ").replace("\n", " ").strip()
+
+def scrape():
+ req = urllib.request.Request(URL, headers={"User-Agent": "Mozilla/5.0 (X11; Linux) lectio"})
+ h = urllib.request.urlopen(req, timeout=60).read().decode("utf-8", "replace")
+ datere = re.compile(r"\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+(\d{1,2})\b")
+ rows = []
+ for tr in re.split(r"<tr\b", h):
+ c = [strip_tags(x) for x in re.findall(r"<td\b[^>]*>(.*?)</td>", tr, re.S)]
+ if len(c) < 10:
+ continue
+ m = datere.search(c[1])
+ if not m:
+ continue
+ mmdd = f"{MONS[m.group(1)]:02d}-{int(m.group(2)):02d}"
+ rank, src = c[3], c[4]
+ if rank.startswith("USA") or "Calif" in rank:
+ continue # national additions not in the universal calendar
+ if not ("PROPER" in src.upper() or "Solemnity" in rank or "Feast" in rank):
+ continue # Common-based memorial -> keep the ferial
+ rows.append((mmdd, c[2], "vigil" in c[2].lower(),
+ {"first": clean_cite(c[5]), "psalm": clean_cite(c[6]),
+ "second": clean_cite(c[7]), "gospel": clean_cite(c[9])}))
+ # one Mass per date: prefer the day Mass over a vigil; later row breaks ties
+ best = {}
+ for mmdd, name, isvig, parts in rows:
+ if not (parts["first"] and parts["gospel"]):
+ continue # need at least first + gospel
+ cur = best.get(mmdd)
+ if cur is None or (cur[0] and not isvig): # replace a vigil with a day Mass
+ best[mmdd] = (isvig, name, parts)
+ return {k: v[2] for k, v in best.items()}
+
+def main():
+ props = scrape()
+ sys.stderr.write(f"scraped {len(props)} proper-reading celebrations\n")
+ text = open(INI).read()
+ head = text[:text.index("\n[")]
+ blocks = re.split(r"\n(?=\[)", text[text.index("\n[") + 1:])
+ # index blocks by date + name
+ out_blocks = []
+ used = set()
+ for b in blocks:
+ m = re.match(r"\[(.+?)\]", b)
+ if not m or m.group(1) == "layer":
+ out_blocks.append(b.rstrip("\n"))
+ continue
+ f = dict(re.findall(r"^([\w.]+)\s*=\s*(.+)$", b, re.M))
+ date = f.get("date", "")
+ # strip any pre-existing reading.* lines (idempotent re-run)
+ b = re.sub(r"^reading\.\w+\s*=.*\n?", "", b, flags=re.M).rstrip("\n")
+ if date in props:
+ p = props[date]
+ add = "".join(f"\nreading.{part} = {p[part]}" for part in
+ ("first", "psalm", "second", "gospel") if p.get(part))
+ b += add
+ used.add(date)
+ out_blocks.append(b)
+ open(INI, "w").write(head.rstrip("\n") + "\n\n" + "\n\n".join(out_blocks) + "\n")
+ missing = sorted(set(props) - used)
+ sys.stderr.write(f"added readings to {len(used)} entries; "
+ f"{len(missing)} CR dates had no matching universal-calendar entry: {missing}\n")
+
+if __name__ == "__main__":
+ main()