aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/__pycache__/genlect-of-sundays-cr.cpython-313.pycbin0 -> 15762 bytes
-rw-r--r--scripts/genlect-of-sundays-cr.py182
2 files changed, 182 insertions, 0 deletions
diff --git a/scripts/__pycache__/genlect-of-sundays-cr.cpython-313.pyc b/scripts/__pycache__/genlect-of-sundays-cr.cpython-313.pyc
new file mode 100644
index 0000000..80579d6
--- /dev/null
+++ b/scripts/__pycache__/genlect-of-sundays-cr.cpython-313.pyc
Binary files differ
diff --git a/scripts/genlect-of-sundays-cr.py b/scripts/genlect-of-sundays-cr.py
new file mode 100644
index 0000000..84dd3f8
--- /dev/null
+++ b/scripts/genlect-of-sundays-cr.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+"""Re-source the Ordinary Form SUNDAY & temporal-solemnity lectionary (the 3-year
+A/B/C cycle) from catholic-resources.org (Fr. Felix Just, S.J.).
+
+The niedziela harvest dropped the SECOND reading on 271 of ~299 Sunday-cycle
+entries (Sunday Mass = first/psalm/second/gospel). catholic-resources.org gives
+the complete readings, keyed by liturgical position. This replaces the temporal
+Sunday and temporal-solemnity entries (Ordinary Time, Advent, Christmas, Lent,
+Easter, Trinity, Corpus Christi, Sacred Heart, Christ the King). Sanctoral
+solemnities/feasts (All Saints, the Assumption, ...) are NOT touched here -- they
+carry inline readings in roman-calendar.ini. Citations only; see NOTICE.
+
+Requires network. From the repo root: python3 scripts/genlect-of-sundays-cr.py
+Rewrites the matching <slug>-<A|B|C> entries in internal/caldata/of-lectionary.ini.
+"""
+import re, sys, html, urllib.request
+
+INI = "internal/caldata/of-lectionary.ini"
+PAGES = ["OrdinaryA", "OrdinaryB", "OrdinaryC", "Advent", "Christmas", "Lent", "Easter", "Solemnities"]
+BASE = "https://www.catholic-resources.org/Lectionary/1998USL-{}.htm"
+
+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")
+# CR Sunday-page abbreviation variants (the Sunday tables abbreviate more tersely)
+reg("Jonah","jon");reg("The Acts","act");reg("Hebrews","hebr");reg("Isaiah","is")
+reg("John","jn");reg("Matthew","mt");reg("Zechariah","zac");reg("Song of Solomon","songs")
+reg("1 Peter","1 petr");reg("2 Maccabees","2 mac");reg("Ezekiel","ez")
+# also accept a spelled-out book name as its own alias (some cells use full names)
+for _full in list(set(BOOK.values())):
+ BOOK.setdefault(_full.lower(), _full)
+
+def clean_cite(raw):
+ s = html.unescape(raw or "")
+ s = re.sub(r"[–—]", "-", s)
+ s = re.sub(r"\([^)]*\)", "", s) # drop parenthetical notes: (#740), (cited…), (diff), (new)…
+ s = re.sub(r"^(opt\.?:|optional:|\d+\))\s*", "", s.strip(), flags=re.I) # cycle-proper "opt:" alternate / numbered option
+ s = re.split(r"\s+or\b", s, maxsplit=1)[0]
+ s = re.sub(r"^(cf\.|see)\s+", "", s.strip(), flags=re.I).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 ""
+ full = BOOK.get(m.group(1).lower().replace(".", "").strip())
+ if not full:
+ return ""
+ return f"{full} {re.sub(r',\s+', ',', m.group(2)).replace(' ', '')}".strip()
+
+def slug_of(name):
+ n = re.sub(r"\s+", " ", name.lower())
+ if "vigil" in n: # a Sunday's vigil Mass -> use the day Mass row instead
+ return None
+ # specific solemnity phrases FIRST (the Year-A rows are verbose, e.g. "Friday
+ # after the Second Sunday after Pentecost: ... Sacred Heart", so a loose
+ # "pentecost"/"trinity" test would mis-fire).
+ if "sacred heart" in n: return "sacred-heart"
+ if "body and blood" in n or "corpus christi" in n: return "corpus-christi"
+ if "most holy trinity" in n or "trinity sunday" in n: return "trinity-sunday"
+ if "christ the king" in n or ("king of the universe" in n): return "christ-the-king"
+ if "holy family" in n: return "holy-family"
+ if "baptism of the lord" in n: return "baptism-of-the-lord"
+ if "epiphany of the lord" in n: return "epiphany"
+ if "mother of god" in n: return "mary-mother-of-god-octave-of-christmas"
+ if "ascension of the lord" in n: return "ascension"
+ if "day of pentecost" in n or "pentecost sunday" in n or n.startswith("pentecost"): return "pentecost"
+ m = re.search(r"(\d+)\w* sunday in ordinary time", n)
+ if m: return f"ordinary-sunday-{m.group(1)}"
+ m = re.search(r"(\d+)\w* sunday of advent", n)
+ if m: return f"advent-sunday-{m.group(1)}"
+ m = re.search(r"(\d+)\w* sunday of lent", n)
+ if m: return f"lent-sunday-{m.group(1)}"
+ m = re.search(r"(\d+)\w* sunday of easter", n)
+ if m:
+ k = int(m.group(1))
+ return "easter-sunday" if k == 1 else "easter-octave-sun" if k == 2 else f"easter-sunday-{k}"
+ if "palm sunday" in n or "passion sunday" in n: return "palm-sunday"
+ if "resurrection of the lord" in n: return "easter-sunday" # Mass of Easter Day
+ if "second sunday after christmas" in n or "sunday within the octave of christmas" in n:
+ return "christmas-sunday-sun"
+ return None # Nativity Day masses, Triduum, etc. -> not a cycle-Sunday
+
+def strip_tags(s):
+ return html.unescape(re.sub(r"<[^>]+>", "", s)).replace("\xa0", " ").replace("\n", " ").strip()
+
+def scrape_page(name):
+ req = urllib.request.Request(BASE.format(name), headers={"User-Agent": "Mozilla/5.0 (X11; Linux) lectio"})
+ h = urllib.request.urlopen(req, timeout=60).read().decode("utf-8", "replace")
+ out = {}
+ 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) < 8 or c[0] == "Date":
+ continue
+ # a name may carry an appended "[2025: Corpus Christi…]" note (this Sunday
+ # is displaced that year); strip a bracketed note that follows real text so
+ # slug_of matches the Sunday, not a word inside the note.
+ name = re.sub(r"(?<=\S)\s*\[[^\]]*\]", "", c[2].replace("\r", " "))
+ cyc = re.search(r"[-–—]\s*(ABC|[ABC])\b", name) # hyphen OR en/em dash; ABC = all cycles
+ slug = slug_of(name)
+ if not slug:
+ continue
+ parts = {"first": clean_cite(c[3]), "psalm": clean_cite(c[4]),
+ "second": clean_cite(c[5]), "gospel": clean_cite(c[7])}
+ if not parts["first"] and not parts["second"]:
+ continue # a procession/notes row with no Mass readings
+ if slug == "palm-sunday":
+ # Palm Sunday's Mass gives cycle-independent first/psalm/second; the
+ # gospel (the Passion) is per cycle, so keep whatever lectio already
+ # has. Apply the readings to all three cycles, without a gospel.
+ for cy in ("A", "B", "C"):
+ out[f"palm-sunday-{cy}"] = {**parts, "gospel": ""}
+ continue
+ if not cyc:
+ continue
+ if cyc.group(1) == "ABC":
+ for cy in ("A", "B", "C"):
+ out.setdefault(f"{slug}-{cy}", parts) # ABC only fills a cycle not given explicitly
+ else:
+ out[f"{slug}-{cyc.group(1)}"] = parts # a specific cycle row always wins
+ return out
+
+def firstbook(c): # book + chapter, for validation
+ m = re.match(r"((?:[1-4] )?[A-Za-z ]+?) (\d+)", c or "")
+ return f"{m.group(1)} {m.group(2)}" if m else (c or "")
+
+def main():
+ cr = {}
+ for p in PAGES:
+ d = scrape_page(p)
+ sys.stderr.write(f"{p}: {len(d)} mapped Sunday entries\n")
+ cr.update(d)
+ text = open(INI).read()
+ head = text[:text.index("\n[")]
+ blocks = {}
+ order = []
+ for b in re.split(r"\n(?=\[)", text[text.index("\n[") + 1:]):
+ m = re.match(r"\[(.+?)\]", b)
+ key = m.group(1) if m else b
+ blocks[key] = b.rstrip("\n"); order.append(key)
+
+ changed = firstdiff = added = 0
+ for key, p in cr.items():
+ if not (p["second"] or p["first"]): # nothing useful to merge
+ continue
+ old = dict(re.findall(r"^(\w+)\s*=\s*(.+)$", blocks.get(key, ""), re.M))
+ if old.get("first") and p["first"] and firstbook(old["first"]) != firstbook(p["first"]):
+ firstdiff += 1
+ sys.stderr.write(f" ~ {key}: first {old['first']!r} -> {p['first']!r}\n")
+ if "second" not in old and p["second"]:
+ changed += 1
+ merged = {part: (p[part] or old.get(part, "")) for part in ("first", "psalm", "second", "gospel")}
+ if not (merged["first"] and merged["gospel"]): # never write an incomplete Sunday
+ continue
+ block = "[" + key + "]\n" + "\n".join(f"{part} = {merged[part]}"
+ for part in ("first", "psalm", "second", "gospel") if merged[part])
+ if key not in blocks:
+ order.append(key); added += 1
+ blocks[key] = block
+ open(INI, "w").write(head.rstrip("\n") + "\n\n" + "\n\n".join(blocks[k] for k in order) + "\n")
+ sys.stderr.write(f"\nmerged Sunday entries; {changed} gained a 2nd reading, {added} new, "
+ f"{firstdiff} first-reading book changes (mapping check)\n")
+
+if __name__ == "__main__":
+ main()