aboutsummaryrefslogtreecommitdiff
path: root/internal/tradlit
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 13:24:28 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 13:24:28 +0200
commitf8772261f0c4feca68eee0300b5d726fd168a534 (patch)
tree4824c91713d5e1e4a18d53b1fcf752043f247d21 /internal/tradlit
parentfbb3345518872346a629e7ccbec81f09532765f9 (diff)
downloadlectio-f8772261f0c4feca68eee0300b5d726fd168a534.tar.gz
lectio-f8772261f0c4feca68eee0300b5d726fd168a534.zip
tradlit: missalemeum 1962 propers -> sections
Diffstat (limited to 'internal/tradlit')
-rw-r--r--internal/tradlit/parse_test.go35
-rw-r--r--internal/tradlit/testdata/2026-07-22.json1
-rw-r--r--internal/tradlit/tradlit.go121
3 files changed, 157 insertions, 0 deletions
diff --git a/internal/tradlit/parse_test.go b/internal/tradlit/parse_test.go
new file mode 100644
index 0000000..a7344cd
--- /dev/null
+++ b/internal/tradlit/parse_test.go
@@ -0,0 +1,35 @@
+package tradlit
+
+import (
+ "os"
+ "testing"
+)
+
+func TestParse(t *testing.T) {
+ body, err := os.ReadFile("testdata/2026-07-22.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ secs, err := Parse(body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var gospel, epistle bool
+ for _, s := range secs {
+ if s.PartID == "evangelium" {
+ gospel = true
+ if s.Citation != "Luke 7:36-50" {
+ t.Errorf("gospel citation = %q", s.Citation)
+ }
+ if len(s.Paragraphs) == 0 {
+ t.Error("gospel has no vernacular text")
+ }
+ }
+ if s.PartID == "lectio" {
+ epistle = true
+ }
+ }
+ if !gospel || !epistle {
+ t.Errorf("missing parts: gospel=%v epistle=%v", gospel, epistle)
+ }
+}
diff --git a/internal/tradlit/testdata/2026-07-22.json b/internal/tradlit/testdata/2026-07-22.json
new file mode 100644
index 0000000..32b366f
--- /dev/null
+++ b/internal/tradlit/testdata/2026-07-22.json
@@ -0,0 +1 @@
+[{"info":{"id":"sancti:07-22:3:w","title":"St. Mary Magdalene","tags":[],"colors":["w"],"date":"2026-07-22","description":"","rank":3,"supplements":[],"tempora":"Feria IV after VIII Sunday after Pentecost","commemorations":[],"displaced":[]},"sections":[{"id":"Introitus","label":"Introit","body":[["*Ps 118:95-96.*\nSinners wait to destroy me, but I pay heed to Your decrees, O Lord. I see that all fulfillment has its limits; broad indeed is Your command. (Alleluja, alleluja.)\n*Ps 118:1*\nHappy are they whose way is blameless, who walk in the law of the Lord.\nGlory Be to the Father…\nSinners wait to destroy me, but I pay heed to Your decrees, O Lord. I see that all fulfillment has its limits; broad indeed is Your command. (Allelúja, allelúja.)","*Ps 118:95-96*\nMe exspectavérunt peccatóres, ut pérderent me: testimónia tua, Dómine, intelléxi: omnis consummatiónis vidi finem: latum mandátum tuum nimis. (Allelúja, allelúja.)\n*Ps 118:1*\nBeáti immaculáti in via: qui ámbulant in lege Dómini.\nGlória Patri…\nMe exspectavérunt peccatóres, ut pérderent me: testimónia tua, Dómine, intelléxi: omnis consummatiónis vidi finem: latum mandátum tuum nimis. (Allelúja, allelúja.)"]]},{"id":"Oratio","label":"Collect","body":[["May the prayers of blessed Mary Magdalen help us, O Lord, Who were moved by her prayers and brought back alive from the grave her brother Lazarus, dead for four days.\nThrough our Lord…","Beátæ Maríæ Magdalénæ, quǽsumus, Dómine, suffrágiis adjuvémur: cujus précibus exorátus, quatriduánum fratrem Lázarum vivum ab ínferis resuscitásti:\nQui vivis…"]]},{"id":"Lectio","label":"Epistle","body":[["Lesson from the book of Canticles\n*Song 3:2-5; 8:6-7*\nI will rise and go about the city; in the streets and crossings I will seek Him Whom my heart loves. I sought Him but I did not find Him. The watchmen came upon me as they made their rounds of the city: Have you seen Him Whom my heart loves? I had hardly left them when I found Him Whom my heart loves. I took hold of Him and would not let Him go till I should bring Him to the home of my mother, to the room of my parent, I adjure you, daughters of Jerusalem, by the gazelles and hinds of the field, do not arouse, do not stir up love before its own time. Set me as a seal on Your heart, as a seal on Your arm; for stern as death is love, relentless as the nether world is devotion; its flames are a blazing fire. Deep waters cannot quench love, nor floods sweep it away. Were one to offer all he owns to purchase love, he would despise it as nothing.","Léctio libri Sapiéntiæ\n*Cant 3:2-5; 8:6-7*\nSurgam, et circuíbo civitátem: per vicos et pláteas quæram, quem díligit ánima mea: quæsívi illum, et non invéni. Invenérunt me vígiles, qui custódiunt civitátem. Num quem díligit ánima mea, vidístis? Páululum cum pertransíssem eos, invéni, quem díligit ánima mea: ténui eum, nec dimíttam, donec introdúcam illum in domum matris meæ et in cubículum genetrícis meæ. Adjúro vos, fíliæ Jerúsalem, per cápreas cervósque campórum, ne suscitétis neque evigiláre faciátis diléctam, donec ipsa velit. Pone me ut signáculum super cor tuum, ut signáculum super bráchium tuum: quia fortis est ut mors diléctio, dura sicut inférnus æmulátio: lámpades ejus lámpades ignis atque flammárum. Aquæ multæ non potuérunt exstínguere caritátem, nec flúmina óbruent illam: si déderit homo omnem substántiam domus suæ pro dilectióne, quasi nihil despíciet eam."]]},{"id":"Graduale","label":"Gradual","body":[["*Ps 44:8*\nYou love justice and hate wickedness.\n℣. Therefore God, your God, has anointed you with the oil of gladness.\nAlleluia, alleluia.\n*Ps 44:3*\nGrace is poured out upon your lips; thus God has blessed you forever. Alleluia.","*Ps 44:8*\nDilexísti justítiam, et odísti iniquitátem.\n℣. Proptérea unxit te Deus, Deus tuus, óleo lætítiæ.\nAllelúja, allelúja.\n*Ps 44:3*\nDiffúsa est grátia in lábiis tuis: proptérea benedíxit te Deus in ætérnum. Allelúja."]]},{"id":"Evangelium","label":"Gospel","body":[["Continuation ☩ of the Holy Gospel according to Luke\n*Luke 7:36-50*\nAt that time, one of the Pharisees asked Jesus to dine with him; so He went into the house of the Pharisee and reclined at table. And behold, a woman in the town who was a sinner, upon learning that He was at table in the Pharisee's house, brought an alabaster jar of ointment; and standing behind Him at His feet, she began to bathe His feet with her tears, and wiped them with the hair of her head, and kissed His feet, and anointed them with ointment. Now when the Pharisee, who had invited Him, saw it, he said to himself, “This man, were He a prophet, would surely know who and what manner of woman this is who is touching Him, for she is a sinner.” And Jesus answered and said to him, “Simon, I have something to say to you.” And he said, “Master, speak.” “A certain money-lender had two debtors; the one owed five hundred denarii, the other fifty. As they had no means of paying, he forgave them both. Which of them, therefore, will love him more?” Simon answered and said, “He, I suppose, to whom he forgave more.” And He said to him, “You have judged rightly.” And turning to the woman, He said to Simon, “Do you see this woman? I came into your house; you gave Me no water for My feet; but she has bathed My feet with her tears, and has wiped them with her hair. You gave Me no kiss; but she, from the moment she entered, has not ceased to kiss My feet. You did not anoint My head with oil; but she has anointed My feet with ointment. Wherefore I say to you, her sins, many as they are, shall be forgiven her, because she has loved much. But he to whom little is forgiven, loves little.” And He said to her, “Your sins are forgiven.” And they who were at table with Him began to say within themselves, “Who is this man, who even forgives sins?” But He said to the woman, “Your faith has saved you; go in peace.”","Sequéntia ☩ sancti Evangélii secúndum Lucam\n*Luc 7:36-50*\nIn illo témpore: Rogábat Jesum quidam de pharisǽis, ut manducáret cum illo. Et ingréssus domum pharisǽi, discúbuit. Et ecce múlier, quæ erat in civitáte peccátrix, ut cognóvit, quod accubuísset in domo pharisǽi, áttulit alabástrum unguénti: et stans retro secus pedes ejus, lácrimis cœpit rigáre pedes ejus, et capíllis cápitis sui tergébat, et osculabátur pedes ejus, et unguénto ungébat. Videns autem pharisǽus, qui vocáverat eum, ait intra se, dicens: Hic si esset Prophéta, sciret útique, quæ et qualis est múlier, quæ tangit eum: quia peccátrix est. Et respóndens Jesus, dixit ad illum: Simon, hábeo tibi áliquid dícere. At ille ait: Magíster, dic. Duo debitóres erant cuidam fœneratóri: unus debébat denários quingéntos, et álius quinquagínta. Non habéntibus illis, unde rédderent, donávit utrísque. Quis ergo eum plus díligit? Respóndens Simon, dixit: Æstimo, quia is, cui plus donávit. At ille dixit ei: Recte judicásti. Et convérsus ad mulíerem, dixit Simóni: Vides hanc mulíerem? Intrávi in domum tuam, aquam pédibus meis non dedísti: hæc autem lácrimis rigávit pedes meos et capíllis suis tersit. Osculum mihi non dedísti: hæc autem, ex quo intrávit, non cessávit osculári pedes meos. Oleo caput meum non unxísti: hæc autem unguénto unxit pedes meos. Propter quod dico tibi: Remittúntur ei peccáta multa, quóniam diléxit multum. Cui autem minus dimíttitur, minus díligit. Dixit autem ad illam: Remittúntur tibi peccáta. Et cœpérunt, qui simul accumbébant, dícere intra se: Quis est hic, qui étiam peccáta dimíttit? Dixit autem ad mulíerem: Fides tua te salvam fecit: vade in pace."]]},{"id":"Offertorium","label":"Offertory","body":[["*Ps 44:10*\nThe daughters of kings have delighted thee in thy glory. The queen stood on thy right hand, in gilded clothing; surrounded with variety. (Allelúja.)","*Ps 44:10*\nFíliæ regum in honóre tuo, ástitit regína a dextris tuis in vestítu deauráto, circúmdata varietáte. (Allelúja.)"]]},{"id":"Secreta","label":"Secret","body":[["O Lord, may our gifts be acceptable to You through the glorious merits of blessed Mary Magdalen, whose offering of homage Your only-begotten Son did mercifully accept.\nWho livest and reignest with God the Father…","Múnera nostra, quǽsumus. Dómine, beátæ Maríæ Magdalénæ gloriósa mérita tibi reddant accépta: cujus oblatiónis obséquium unigénitus Fílius tuus cleménter suscépit impénsum:\nQui tecum…"]]},{"id":"Prefatio","label":"Preface","body":[["*Common*\nIt is truly meet and just, and profitable unto salvation, that we should at all times, and in all places, give thanks to Thee, O Holy Lord, Father Almighty, eternal God, through Christ, our Lord. Through whom the Angels praise Thy Majesty, the Dominions adore it, the Powers are in awe. Which the heavens and the hosts of heaven together with the blessed Seraphim joyfully do magnify. And do Thou command that it be permitted us to join with them in confessing Thee, while we say with lowly praise:","*Communis*\nVere dignum et justum est, æquum et salutáre, nos tibi semper et ubíque grátias ágere: Dómine sancte, Pater omnípotens, ætérne Deus: per Christum, Dóminum nostrum. Per quem majestátem tuam laudant Angeli, adórant Dominatiónes, tremunt Potestátes. Cæli cælorúmque Virtútes ac beáta Séraphim sócia exsultatióne concélebrant. Cum quibus et nostras voces ut admítti jubeas, deprecámur, súpplici confessione dicéntes:"]]},{"id":"Communio","label":"Communion","body":[["*Ps 118:121-122; 118:128*\nI have fulfilled just ordinances, O Lord; let not the proud oppress me. For in all Your precepts I go forward; every false way I hate. (Alleluja.)","*Ps 118:121; 118:122; 118:128*\nFeci judícium et justítiam, Dómine, non calumniéntur mihi supérbi: ad ómnia mandáta tua dirigébar, omnem viam iniquitátis ódio hábui. (Allelúja.)"]]},{"id":"Postcommunio","label":"Postcommunion","body":[["After receiving Your Body and precious Blood, the one and only saving remedy, we beseech You, O Lord, that, under the protection of blessed Mary Magdalen, we may be freed from all evils.\nWho livest…","Sumpto, quǽsumus, Dómine, único ac salutári remédio, Córpore et Sánguine tuo pretióso: ab ómnibus malis, sanctæ Maríæ Magdalénæ patrocíniis, eruámur:\nQui vivis…"]]}]}] \ No newline at end of file
diff --git a/internal/tradlit/tradlit.go b/internal/tradlit/tradlit.go
new file mode 100644
index 0000000..edb9ce0
--- /dev/null
+++ b/internal/tradlit/tradlit.go
@@ -0,0 +1,121 @@
+// Package tradlit fetches and parses the Traditional (1962 Missal) Latin
+// propers from the missalemeum JSON API into the shared liturgy.Section
+// type, so the rest of the app can treat the traditional and modern
+// (niedziela.pl) lectionaries interchangeably.
+package tradlit
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "regexp"
+ "strings"
+
+ "github.com/lukaszkasprzak/lectio/internal/liturgy"
+)
+
+// baseURL is the missalemeum proper-of-the-day API template ("%s" is lang,
+// then date, YYYY-MM-DD). It is a package var so tests can point it at an
+// httptest server.
+var baseURL = "https://www.missalemeum.com/%s/api/v5/proper/%s"
+
+// userAgent is sent on every fetch; matches the browser UA used elsewhere
+// in this project (see the plan's Global Constraints).
+const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " +
+ "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
+
+// citationRe matches the first *...* marker in a section's body text, e.g.
+// "*Luke 7:36-50*" or "*Ps 44:2*".
+var citationRe = regexp.MustCompile(`\*([^*\n]+)\*`)
+
+// apiResponse mirrors the shape of the missalemeum proper-of-the-day API:
+// a single-element list of { info, sections }.
+type apiResponse struct {
+ Info struct {
+ Title string `json:"title"`
+ } `json:"info"`
+ Sections []apiSection `json:"sections"`
+}
+
+type apiSection struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ Body [][]string `json:"body"`
+}
+
+// Parse decodes a missalemeum proper-of-the-day API response body into
+// liturgy.Sections. Sections with an empty id or empty body are skipped.
+func Parse(jsonBody []byte) ([]liturgy.Section, error) {
+ var resp []apiResponse
+ if err := json.Unmarshal(jsonBody, &resp); err != nil {
+ return nil, fmt.Errorf("tradlit: parse: %w", err)
+ }
+ if len(resp) == 0 {
+ return nil, fmt.Errorf("tradlit: parse: empty response")
+ }
+
+ var out []liturgy.Section
+ for _, sec := range resp[0].Sections {
+ if sec.ID == "" || len(sec.Body) == 0 || len(sec.Body[0]) == 0 {
+ continue
+ }
+
+ text := strings.Join(sec.Body[0], "\n")
+
+ var lines []string
+ for _, line := range strings.Split(text, "\n") {
+ line = strings.TrimSpace(line)
+ if line != "" {
+ lines = append(lines, line)
+ }
+ }
+
+ citation := ""
+ if m := citationRe.FindStringSubmatch(text); m != nil {
+ citation = strings.TrimSpace(m[1])
+ }
+
+ out = append(out, liturgy.Section{
+ Heading: sec.Label,
+ Citation: citation,
+ PartID: strings.ToLower(sec.ID),
+ Paragraphs: [][]string{lines},
+ })
+ }
+ return out, nil
+}
+
+// Load fetches a day's traditional propers from the missalemeum API
+// (https://www.missalemeum.com/{lang}/api/v5/proper/{date}) and parses
+// them. On HTTP 404 (no propers published for that date) it returns a
+// clear error rather than attempting to parse.
+func Load(date, lang string) ([]liturgy.Section, error) {
+ url := fmt.Sprintf(baseURL, lang, date)
+
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("tradlit: load: %w", err)
+ }
+ req.Header.Set("User-Agent", userAgent)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("tradlit: load %s: %w", date, err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, fmt.Errorf("tradlit: no propers published for %s", date)
+ }
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("tradlit: load %s: unexpected status %s", date, resp.Status)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("tradlit: load %s: %w", date, err)
+ }
+
+ return Parse(body)
+}