diff --git a/src/data/propers/common/benedictus.yml b/src/data/propers/common/benedictus.yml index 8a9c7f7..8720f1e 100644 --- a/src/data/propers/common/benedictus.yml +++ b/src/data/propers/common/benedictus.yml @@ -1,7 +1,10 @@ # Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds live # query. Fixed year-round, same shape as nunc-dimittis.yml — the framing # antiphon is resolved separately, by day (see hours/resolve-common.ts's -# getBenedictusAntiphon), not stored here. +# getBenedictusAntiphon), not stored here. The closing Gloria Patri isn't +# stored here either — it's attached centrally (hours/index.ts) so it can +# be omitted during the Sacred Triduum (live-verified: Divino Afflatu 1954 +# and Tridentine 1906 both omit it at Lauds of Holy Thursday). id: benedictus text: la: | @@ -17,8 +20,6 @@ text: Ad dandam sciéntiam salútis plebi ejus: * in remissiónem peccatórum eórum: Per víscera misericórdiæ Dei nostri: * in quibus visitávit nos, óriens ex alto: Illumináre his, qui in ténebris, et in umbra mortis sedent: * ad dirigéndos pedes nostros in viam pacis. - V. Glória Patri, et Fílio, * et Spirítui Sancto. - R. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen. en: | Blessed be the Lord ✠ God of Israel; * because he hath visited and wrought the redemption of his people: And hath raised up an horn of salvation to us, * in the house of David his servant: @@ -32,8 +33,6 @@ text: To give knowledge of salvation to his people, * unto the remission of their sins: Through the bowels of the mercy of our God, * in which the Orient from on high hath visited us: To enlighten them that sit in darkness, and in the shadow of death: * to direct our feet into the way of peace. - V. Glory be to the Father, and to the Son, * and to the Holy Ghost. - R. As it was in the beginning, is now, * and ever shall be, world without end. Amen. citation: la: "Luc. 1:68-79" en: "Luke 1:68-79" diff --git a/src/data/propers/common/magnificat.yml b/src/data/propers/common/magnificat.yml index 1ddcc82..442fb73 100644 --- a/src/data/propers/common/magnificat.yml +++ b/src/data/propers/common/magnificat.yml @@ -4,7 +4,9 @@ # year-round, same shape as benedictus.yml/nunc-dimittis.yml — the framing # antiphon is resolved separately, by day (see hours/resolve-common.ts's # getMagnificatAntiphon), not stored here. Not live-query-verified (see -# TODO.md) — read directly from source. +# TODO.md) — read directly from source. The closing Gloria Patri isn't +# stored here either — it's attached centrally (hours/index.ts) so it can +# be omitted during the Sacred Triduum, same as every other canticle/psalm. id: magnificat text: la: | @@ -18,8 +20,6 @@ text: Esuriéntes implévit bonis: * et dívites dimísit inánes. Suscépit Israël púerum suum: * recordátus misericórdiæ suæ. Sicut locútus est ad patres nostros: * Ábraham, et sémini ejus in sǽcula. - V. Glória Patri, et Fílio, * et Spirítui Sancto. - R. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen. en: | My soul ✠ doth magnify the Lord. And my spirit hath rejoiced * in God my Saviour. @@ -31,8 +31,6 @@ text: He hath filled the hungry with good things; * and the rich he hath sent empty away. He hath received Israel his servant, * being mindful of his mercy: As he spoke to our fathers, * to Abraham and to his seed for ever. - V. Glory be to the Father, and to the Son, * and to the Holy Ghost. - R. As it was in the beginning, is now, * and ever shall be, world without end. Amen. status: la: verified en: verified diff --git a/src/data/propers/common/nunc-dimittis.yml b/src/data/propers/common/nunc-dimittis.yml index 28239e0..a13604e 100644 --- a/src/data/propers/common/nunc-dimittis.yml +++ b/src/data/propers/common/nunc-dimittis.yml @@ -1,7 +1,10 @@ # Verified against Divinum Officium (Tridentine 1906 run, 2026-08-25 — # Monastic Compline doesn't have this canticle at all; it's added from the # Tridentine 1906 form by explicit choice). The antiphon that frames it -# ("Salva nos") is a separate proper — see nunc-dimittis-antiphon.yml. +# ("Salva nos") is a separate proper — see nunc-dimittis-antiphon.yml. The +# closing Gloria Patri isn't stored here either — it's attached centrally +# (hours/index.ts) so it can be omitted during the Sacred Triduum, same as +# every other canticle/psalm. id: nunc-dimittis text: la: | @@ -9,15 +12,11 @@ text: Quia vidérunt óculi mei * salutáre tuum, Quod parásti * ante fáciem ómnium populórum, Lumen ad revelatiónem géntium, * et glóriam plebis tuæ Israël. - V. Glória Patri, et Fílio, * et Spirítui Sancto. - R. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen. en: | Now ✠ thou dost dismiss thy servant, O Lord, * according to thy word in peace; Because my eyes have seen * thy salvation, Which thou hast prepared * before the face of all peoples: A light to the revelation of the Gentiles, * and the glory of thy people Israel. - V. Glory be to the Father, and to the Son, * and to the Holy Ghost. - R. As it was in the beginning, is now, * and ever shall be, world without end. Amen. citation: la: "Luc. 2:29-32" en: "Luke 2:29-32" diff --git a/src/hours/index.ts b/src/hours/index.ts index 030f979..df14a7b 100644 --- a/src/hours/index.ts +++ b/src/hours/index.ts @@ -26,13 +26,16 @@ const registry: Record = { matins: matins.resolveOrdo, }; -/** Attaches the closing Gloria Patri to every plain psalm part (recursing - * into 'nocturn-psalmody' groups), so no per-hour builder has to remember - * to do it — and so the Sacred Triduum omission (resolveGloriaPatri) is - * enforced in exactly one place rather than once per hour file. */ +/** Attaches the closing Gloria Patri to every plain psalm and canticle part + * (recursing into 'nocturn-psalmody' groups), so no per-hour builder has to + * remember to do it — and so the Sacred Triduum omission + * (resolveGloriaPatri) is enforced in exactly one place rather than once + * per hour file. Live-verified (Divino Afflatu 1954, Tridentine 1906) that + * canticles (e.g. Benedictus at Lauds of Holy Thursday) follow the exact + * same Triduum-only omission as plain psalms — not a separate rule. */ function withGloriaPatri(parts: ResolvedPart[], gloriaPatri: ResolvedText | undefined): ResolvedPart[] { return parts.map((part) => { - if (part.kind === 'psalm') { + if (part.kind === 'psalm' || part.kind === 'canticle') { return { ...part, gloriaPatri }; } if (part.kind === 'nocturn-psalmody') { diff --git a/src/hours/lauds.ts b/src/hours/lauds.ts index 87d447e..89e3266 100644 --- a/src/hours/lauds.ts +++ b/src/hours/lauds.ts @@ -1,7 +1,6 @@ import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types'; import type { LiturgicalDay, Weekday } from '../calendar/types'; import { resolveDay, isSundayOrFeast, activeOctavesFor } from '../calendar'; -import { isInTriduum } from '../calendar/temporal'; import { getDayLabel } from '../calendar/day-label'; import { getTemporalFeastRecord } from '../calendar/temporal-feasts'; import { getPsalmVerses } from '../psalter'; @@ -93,22 +92,16 @@ function psalmParts(numbers: number[], antiphon: BilingualText, opening: Resolve const STATUS_RANK = { verified: 0, draft: 1, missing: 2 } as const; -const GLORIA_PATRI = { - la: 'V. Glória Patri, et Fílio, * et Spirítui Sancto.\nR. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen.', - en: 'V. Glory be to the Father, and to the Son, * and to the Holy Ghost.\nR. As it was in the beginning, is now, * and ever shall be, world without end. Amen.', -}; - /** Joins a canticle's verse array (or a `[start, end)` slice of it — see * Saturday's split canticle below) into one flowing text block, the same * shape nunc-dimittis/benedictus already use — Lauds' weekday canticles * are stored verse-by-verse (see lauds-canticles.ts) purely because that's * how they were transcribed, not because callers need per-verse access. - * Appends the fixed Gloria Patri line every canticle (or canticle part) - * ends with in real practice, same wording as benedictus.yml's own - * trailing line — omitted during the Sacred Triduum, live-verified against - * both Divino Afflatu 1954 and Tridentine 1906 (Benedictus at Lauds of - * Holy Thursday omits it same as any psalm). */ -function canticleText(canticleId: string, slice: [number, number] | undefined, omitGloriaPatri: boolean): ResolvedText { + * Does NOT append a closing Gloria Patri — that's attached centrally + * (hours/index.ts) as its own 'canticle' field, same as every other + * canticle/psalm, so the Sacred Triduum omission only has to be handled + * in one place. */ +function canticleText(canticleId: string, slice?: [number, number]): ResolvedText { const canticle = getCanticle(canticleId); const verses = slice ? canticle.verses.slice(slice[0], slice[1]) : canticle.verses; const text: Partial> = {}; @@ -118,7 +111,7 @@ function canticleText(canticleId: string, slice: [number, number] | undefined, o if (lines.length === 0) { continue; } - text[lang] = omitGloriaPatri ? lines.join('\n') : `${lines.join('\n')}\n${GLORIA_PATRI[lang]}`; + text[lang] = lines.join('\n'); let worst: 'verified' | 'draft' | 'missing' = 'verified'; for (const v of verses) { const s = v.status[lang] ?? 'missing'; @@ -153,7 +146,6 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] { const canticle = getCanticle(wd.canticle.id); const canticleOpening = opening(wd.canticle.antiphon); const canticleClosing = splitNamedAntiphon(verifiedText(wd.canticle.antiphon)).full; - const omitGloriaPatri = isInTriduum(day.date); if (wd.canticle.split) { // Said in two pieces, own Gloria Patri each, one shared antiphon // framing both (opening before the first, full repeated only after @@ -162,19 +154,19 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] { kind: 'canticle', canticleId: canticle.id, antiphon: canticleOpening, - text: canticleText(canticle.id, [0, wd.canticle.split], omitGloriaPatri), + text: canticleText(canticle.id, [0, wd.canticle.split]), }); parts.push({ kind: 'canticle', canticleId: canticle.id, - text: canticleText(canticle.id, [wd.canticle.split, canticle.verses.length], omitGloriaPatri), + text: canticleText(canticle.id, [wd.canticle.split, canticle.verses.length]), }); } else { parts.push({ kind: 'canticle', canticleId: canticle.id, antiphon: canticleOpening, - text: canticleText(canticle.id, undefined, omitGloriaPatri), + text: canticleText(canticle.id), }); } parts.push({ kind: 'antiphon', text: canticleClosing }); diff --git a/src/hours/types.ts b/src/hours/types.ts index f7d93ca..20e2930 100644 --- a/src/hours/types.ts +++ b/src/hours/types.ts @@ -204,7 +204,9 @@ export type ResolvedPart = // Sacred Triduum (calendar/temporal.ts's isInTriduum) — never omit it // when building a psalm part yourself. | { kind: 'psalm'; psalmNumber: number; antiphon?: ResolvedText; verses: ResolvedVerse[]; gloriaPatri?: ResolvedText } - | { kind: 'canticle'; canticleId: string; text: ResolvedText; antiphon?: ResolvedText } + // `gloriaPatri` filled in centrally, same as the 'psalm' kind above — + // see that field's doc comment. + | { kind: 'canticle'; canticleId: string; text: ResolvedText; antiphon?: ResolvedText; gloriaPatri?: ResolvedText } // A standalone antiphon, said on its own rather than framing a psalm — // see 'closing-antiphon' above. | { kind: 'antiphon'; text: ResolvedText } diff --git a/src/ui/hour-view.ts b/src/ui/hour-view.ts index 39aa3ec..656e99b 100644 --- a/src/ui/hour-view.ts +++ b/src/ui/hour-view.ts @@ -109,6 +109,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {

${escapeHtml(title)}

${renderCitation(part.text)} ${renderColumns(part.text, languages)} + ${part.gloriaPatri ? `
${renderColumns(part.gloriaPatri, languages)}
` : ''} `; } diff --git a/tests/hours/lauds.test.ts b/tests/hours/lauds.test.ts index ec5f574..53b7955 100644 --- a/tests/hours/lauds.test.ts +++ b/tests/hours/lauds.test.ts @@ -78,8 +78,8 @@ describe('resolveOrdo("lauds", ...)', () => { expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Audíte, cæli'); expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Ignis succénsus est'); // Each part gets its own Gloria Patri. - expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Glória Patri'); - expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Glória Patri'); + expect(part1?.kind === 'canticle' ? part1.gloriaPatri?.text.la : undefined).toContain('Glória Patri'); + expect(part2?.kind === 'canticle' ? part2.gloriaPatri?.text.la : undefined).toContain('Glória Patri'); const repeatIndex = ordo.parts.indexOf(part2!) + 1; const repeat = ordo.parts[repeatIndex];