Add closing Gloria Patri after psalms, omitted during the Sacred Triduum
Psalms across every hour rendered with no closing Gloria Patri at all. Add it centrally (hours/index.ts) rather than per-hour, using a new calendar/temporal.ts isInTriduum helper so it's correctly dropped from Maundy Thursday through Holy Saturday and nowhere else — live-verified against both of this app's actual source tracks (Divino Afflatu 1954 and Tridentine 1906), not just the modern Rubrics 1960 default. Also fixes Lauds' existing but Triduum-blind canticle Gloria Patri append.
This commit is contained in:
@@ -126,6 +126,18 @@ export function easterOffsetOf(isoDate: string): number {
|
|||||||
return daysBetween(toIsoDate(easterSunday(year)), isoDate);
|
return daysBetween(toIsoDate(easterSunday(year)), isoDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The Sacred Triduum: Maundy/Holy Thursday through Holy Saturday
|
||||||
|
* (inclusive). Doesn't line up with `season` (which resolves this window
|
||||||
|
* to 'passiontide', same as the two weeks before it) — see
|
||||||
|
* hours/marian-antiphon.ts's isCandlemasToHolyWednesday for the same
|
||||||
|
* direct-date-check pattern used for another window `season` can't
|
||||||
|
* represent. Gloria Patri is omitted after psalms only in this narrower
|
||||||
|
* window, not throughout Passiontide/Holy Week generally. */
|
||||||
|
export function isInTriduum(isoDate: string): boolean {
|
||||||
|
const offset = easterOffsetOf(isoDate);
|
||||||
|
return offset >= -3 && offset <= -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A day's precedence category under the temporal cycle alone — see
|
* A day's precedence category under the temporal cycle alone — see
|
||||||
* calendar/types.ts's TemporalCategory doc comment and
|
* calendar/types.ts's TemporalCategory doc comment and
|
||||||
|
|||||||
+22
-2
@@ -1,4 +1,4 @@
|
|||||||
import type { HourId, ResolvedOrdo } from './types';
|
import type { HourId, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||||
import { HOUR_IDS } from './types';
|
import { HOUR_IDS } from './types';
|
||||||
import * as prime from './prime';
|
import * as prime from './prime';
|
||||||
import * as compline from './compline';
|
import * as compline from './compline';
|
||||||
@@ -8,6 +8,8 @@ import * as none from './none';
|
|||||||
import * as lauds from './lauds';
|
import * as lauds from './lauds';
|
||||||
import * as vespers from './vespers';
|
import * as vespers from './vespers';
|
||||||
import * as matins from './matins';
|
import * as matins from './matins';
|
||||||
|
import { resolveDay } from '../calendar';
|
||||||
|
import { resolveGloriaPatri } from './resolve-common';
|
||||||
|
|
||||||
type Resolver = (date: string) => ResolvedOrdo;
|
type Resolver = (date: string) => ResolvedOrdo;
|
||||||
|
|
||||||
@@ -24,8 +26,26 @@ const registry: Record<HourId, Resolver> = {
|
|||||||
matins: matins.resolveOrdo,
|
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. */
|
||||||
|
function withGloriaPatri(parts: ResolvedPart[], gloriaPatri: ResolvedText | undefined): ResolvedPart[] {
|
||||||
|
return parts.map((part) => {
|
||||||
|
if (part.kind === 'psalm') {
|
||||||
|
return { ...part, gloriaPatri };
|
||||||
|
}
|
||||||
|
if (part.kind === 'nocturn-psalmody') {
|
||||||
|
return { ...part, psalms: withGloriaPatri(part.psalms, gloriaPatri) };
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveOrdo(hourId: HourId, date: string): ResolvedOrdo {
|
export function resolveOrdo(hourId: HourId, date: string): ResolvedOrdo {
|
||||||
return registry[hourId](date);
|
const ordo = registry[hourId](date);
|
||||||
|
const gloriaPatri = resolveGloriaPatri(resolveDay(date));
|
||||||
|
return { ...ordo, parts: withGloriaPatri(ordo.parts, gloriaPatri) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { HOUR_IDS };
|
export { HOUR_IDS };
|
||||||
|
|||||||
+10
-6
@@ -1,6 +1,7 @@
|
|||||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||||
import { resolveDay, isSundayOrFeast, activeOctavesFor } from '../calendar';
|
import { resolveDay, isSundayOrFeast, activeOctavesFor } from '../calendar';
|
||||||
|
import { isInTriduum } from '../calendar/temporal';
|
||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
@@ -104,8 +105,10 @@ const GLORIA_PATRI = {
|
|||||||
* how they were transcribed, not because callers need per-verse access.
|
* how they were transcribed, not because callers need per-verse access.
|
||||||
* Appends the fixed Gloria Patri line every canticle (or canticle part)
|
* Appends the fixed Gloria Patri line every canticle (or canticle part)
|
||||||
* ends with in real practice, same wording as benedictus.yml's own
|
* ends with in real practice, same wording as benedictus.yml's own
|
||||||
* trailing line. */
|
* trailing line — omitted during the Sacred Triduum, live-verified against
|
||||||
function canticleText(canticleId: string, slice?: [number, number]): ResolvedText {
|
* 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 {
|
||||||
const canticle = getCanticle(canticleId);
|
const canticle = getCanticle(canticleId);
|
||||||
const verses = slice ? canticle.verses.slice(slice[0], slice[1]) : canticle.verses;
|
const verses = slice ? canticle.verses.slice(slice[0], slice[1]) : canticle.verses;
|
||||||
const text: Partial<Record<string, string>> = {};
|
const text: Partial<Record<string, string>> = {};
|
||||||
@@ -115,7 +118,7 @@ function canticleText(canticleId: string, slice?: [number, number]): ResolvedTex
|
|||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
text[lang] = `${lines.join('\n')}\n${GLORIA_PATRI[lang]}`;
|
text[lang] = omitGloriaPatri ? lines.join('\n') : `${lines.join('\n')}\n${GLORIA_PATRI[lang]}`;
|
||||||
let worst: 'verified' | 'draft' | 'missing' = 'verified';
|
let worst: 'verified' | 'draft' | 'missing' = 'verified';
|
||||||
for (const v of verses) {
|
for (const v of verses) {
|
||||||
const s = v.status[lang] ?? 'missing';
|
const s = v.status[lang] ?? 'missing';
|
||||||
@@ -150,6 +153,7 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
const canticle = getCanticle(wd.canticle.id);
|
const canticle = getCanticle(wd.canticle.id);
|
||||||
const canticleOpening = opening(wd.canticle.antiphon);
|
const canticleOpening = opening(wd.canticle.antiphon);
|
||||||
const canticleClosing = splitNamedAntiphon(verifiedText(wd.canticle.antiphon)).full;
|
const canticleClosing = splitNamedAntiphon(verifiedText(wd.canticle.antiphon)).full;
|
||||||
|
const omitGloriaPatri = isInTriduum(day.date);
|
||||||
if (wd.canticle.split) {
|
if (wd.canticle.split) {
|
||||||
// Said in two pieces, own Gloria Patri each, one shared antiphon
|
// Said in two pieces, own Gloria Patri each, one shared antiphon
|
||||||
// framing both (opening before the first, full repeated only after
|
// framing both (opening before the first, full repeated only after
|
||||||
@@ -158,19 +162,19 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
kind: 'canticle',
|
kind: 'canticle',
|
||||||
canticleId: canticle.id,
|
canticleId: canticle.id,
|
||||||
antiphon: canticleOpening,
|
antiphon: canticleOpening,
|
||||||
text: canticleText(canticle.id, [0, wd.canticle.split]),
|
text: canticleText(canticle.id, [0, wd.canticle.split], omitGloriaPatri),
|
||||||
});
|
});
|
||||||
parts.push({
|
parts.push({
|
||||||
kind: 'canticle',
|
kind: 'canticle',
|
||||||
canticleId: canticle.id,
|
canticleId: canticle.id,
|
||||||
text: canticleText(canticle.id, [wd.canticle.split, canticle.verses.length]),
|
text: canticleText(canticle.id, [wd.canticle.split, canticle.verses.length], omitGloriaPatri),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
parts.push({
|
parts.push({
|
||||||
kind: 'canticle',
|
kind: 'canticle',
|
||||||
canticleId: canticle.id,
|
canticleId: canticle.id,
|
||||||
antiphon: canticleOpening,
|
antiphon: canticleOpening,
|
||||||
text: canticleText(canticle.id),
|
text: canticleText(canticle.id, undefined, omitGloriaPatri),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
parts.push({ kind: 'antiphon', text: canticleClosing });
|
parts.push({ kind: 'antiphon', text: canticleClosing });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getCommonProper, getTemporalProper } from '../propers';
|
|||||||
import { getSaintRecord } from '../calendar/feasts';
|
import { getSaintRecord } from '../calendar/feasts';
|
||||||
import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, isAtLeast } from '../calendar';
|
import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, isAtLeast } from '../calendar';
|
||||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||||
|
import { isInTriduum } from '../calendar/temporal';
|
||||||
import { splitAntiphon } from './antiphon';
|
import { splitAntiphon } from './antiphon';
|
||||||
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
||||||
|
|
||||||
@@ -677,3 +678,22 @@ export function splitNamedAntiphon(antiphon: ResolvedText): {
|
|||||||
}
|
}
|
||||||
return { incipit: { text: incipitText, status: incipitStatus }, full: { text: fullText, status: fullStatus } };
|
return { incipit: { text: incipitText, status: incipitStatus }, full: { text: fullText, status: fullStatus } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fixed wording, same as lauds.ts's weekday-canticle Gloria Patri —
|
||||||
|
* appended after every psalm (hours/index.ts) except during the Sacred
|
||||||
|
* Triduum. Not "verified" via verifiedText() because it's boilerplate
|
||||||
|
* used everywhere, not a sourced proper text. */
|
||||||
|
const GLORIA_PATRI: BilingualText = {
|
||||||
|
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.',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The closing Gloria Patri for a psalm, or `undefined` during the Sacred
|
||||||
|
* Triduum (calendar/temporal.ts's isInTriduum) when it's omitted
|
||||||
|
* entirely — not just seasonally varied wording, an actual omission. */
|
||||||
|
export function resolveGloriaPatri(day: LiturgicalDay): ResolvedText | undefined {
|
||||||
|
if (isInTriduum(day.date)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return verifiedText(GLORIA_PATRI);
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -199,7 +199,11 @@ export interface HourDefinition {
|
|||||||
export type ResolvedPart =
|
export type ResolvedPart =
|
||||||
| { kind: 'hymn' | 'chapter' | 'responsory' | 'versicle' | 'prayer'; text: ResolvedText }
|
| { kind: 'hymn' | 'chapter' | 'responsory' | 'versicle' | 'prayer'; text: ResolvedText }
|
||||||
| { kind: 'preces'; text: ResolvedText; label?: string }
|
| { kind: 'preces'; text: ResolvedText; label?: string }
|
||||||
| { kind: 'psalm'; psalmNumber: number; antiphon?: ResolvedText; verses: ResolvedVerse[] }
|
// `gloriaPatri` is filled in centrally (hours/index.ts, after every hour
|
||||||
|
// builder returns) rather than per-hour, so it's absent only during the
|
||||||
|
// 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 }
|
| { kind: 'canticle'; canticleId: string; text: ResolvedText; antiphon?: ResolvedText }
|
||||||
// A standalone antiphon, said on its own rather than framing a psalm —
|
// A standalone antiphon, said on its own rather than framing a psalm —
|
||||||
// see 'closing-antiphon' above.
|
// see 'closing-antiphon' above.
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
|||||||
<ol class="psalm-verses">
|
<ol class="psalm-verses">
|
||||||
${part.verses.map((v) => `<li class="psalm-verse">${renderColumns(v, languages)}</li>`).join('')}
|
${part.verses.map((v) => `<li class="psalm-verse">${renderColumns(v, languages)}</li>`).join('')}
|
||||||
</ol>
|
</ol>
|
||||||
|
${part.gloriaPatri ? `<div class="ordo-part-gloria-patri">${renderColumns(part.gloriaPatri, languages)}</div>` : ''}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user