compline: build the full hour, sharing Prime's resolver helpers
Deploy / deploy (push) Successful in 38s
Deploy / deploy (push) Successful in 38s
Ordo is Monastic 1617 as a base, with deliberate Tridentine 1906 additions: the "In manus tuas" short responsory, the Nunc Dimittis with its "Salva nos" antiphon, the fuller Preces (Monastic's own is shorter), and Lent's "Christe, qui lux es et dies" hymn replacement (English translation marked draft, pending a real historical one). Psalms 4, 90, 133 are fixed daily with real verified text, unlike Prime's weekday-rotated psalms. The closing Marian antiphon now picks Alma Redemptoris Mater / Ave Regina Caelorum / Regina Caeli / Salve Regina by real season, using the calendar work from the previous commit — except Ave Regina Caelorum's own window (Candlemas through the day before Maundy Thursday) doesn't align to any single season value, so that one case checks the real date directly instead of going through the by-season table. resolve-common.ts extracts the resolve/status/doxology/antiphon helpers Prime already had into something Compline can share; a few propers files lose their "prime-" prefix now that both hours use them.
This commit is contained in:
+18
-58
@@ -1,76 +1,26 @@
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveDay, isSundayOrFeast } from '../calendar';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getCommonProper } from '../propers';
|
||||
import { getMartyrologyEntryFor } from '../martyrology';
|
||||
import { getRegulaReadingFor } from '../regula';
|
||||
import { getChapterResponsoryId } from './chapter-responsory';
|
||||
import { getHymnDoxologyId } from './hymn-doxology';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { splitAntiphon, isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { resolveCommon, appendDoxology, splitNamedAntiphon } from './resolve-common';
|
||||
import primeDefinitionData from '../data/hours/prime.yml';
|
||||
import antiphonsData from '../data/hours/prime-antiphons.yml';
|
||||
|
||||
const primeDefinition = primeDefinitionData as HourDefinition;
|
||||
const antiphons = antiphonsData as Record<Weekday, Partial<Record<string, string>>>;
|
||||
|
||||
function resolveCommon(id: string): ResolvedText {
|
||||
const proper = getCommonProper(id);
|
||||
return { text: proper.text, status: proper.status, citation: proper.citation };
|
||||
}
|
||||
|
||||
function verifiedText(text: Partial<Record<string, string>>): ResolvedText {
|
||||
const status: Partial<Record<string, 'verified'>> = {};
|
||||
for (const lang of Object.keys(text)) {
|
||||
status[lang] = 'verified';
|
||||
}
|
||||
return { text, status };
|
||||
}
|
||||
|
||||
const STATUS_RANK = { verified: 0, draft: 1, missing: 2 } as const;
|
||||
|
||||
/** Joins a hymn's body with its (seasonally-variable) final doxology
|
||||
* stanza, per language — status is the worse of the two per language. */
|
||||
function appendDoxology(body: ResolvedText, doxology: ResolvedText): ResolvedText {
|
||||
const text: Partial<Record<string, string>> = { ...body.text };
|
||||
const status: Partial<Record<string, 'verified' | 'draft' | 'missing'>> = { ...body.status };
|
||||
for (const lang of Object.keys(doxology.text)) {
|
||||
const doxText = doxology.text[lang];
|
||||
if (doxText) {
|
||||
text[lang] = text[lang] ? `${text[lang]}\n\n${doxText}` : doxText;
|
||||
}
|
||||
const bodyStatus = status[lang] ?? 'missing';
|
||||
const doxStatus = doxology.status[lang] ?? 'missing';
|
||||
status[lang] = STATUS_RANK[doxStatus] > STATUS_RANK[bodyStatus] ? doxStatus : bodyStatus;
|
||||
}
|
||||
return { text, status };
|
||||
}
|
||||
|
||||
/** Splits a weekday's stored antiphon (one string per language, each with
|
||||
* an embedded "*") into its incipit and full forms, per language — each
|
||||
* prefixed "Ant. " inline, the same way "V."/"R." are baked directly into
|
||||
* versicle text rather than rendered as a separate UI marker. */
|
||||
function splitWeekdayAntiphon(weekday: Weekday): { incipit: ResolvedText; full: ResolvedText } {
|
||||
const incipitText: Partial<Record<string, string>> = {};
|
||||
const fullText: Partial<Record<string, string>> = {};
|
||||
for (const [lang, text] of Object.entries(antiphons[weekday])) {
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
const split = splitAntiphon(text);
|
||||
incipitText[lang] = `Ant. ${split.incipit}`;
|
||||
fullText[lang] = `Ant. ${split.full}`;
|
||||
}
|
||||
return { incipit: verifiedText(incipitText), full: verifiedText(fullText) };
|
||||
}
|
||||
|
||||
function resolvePart(part: HourPart, date: string, day: LiturgicalDay): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'hymn': {
|
||||
const body = resolveCommon(part.textRef.id);
|
||||
const doxology = resolveCommon(getHymnDoxologyId(day.season, day.occurring));
|
||||
const doxology = resolveCommon(getHymnDoxologyId(day.season, day.occurring, 'hymn-doxology-per-annum'));
|
||||
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
|
||||
}
|
||||
case 'chapter':
|
||||
@@ -79,6 +29,9 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
case 'prayer':
|
||||
return [{ kind: part.kind, text: resolveCommon(part.textRef.id) }];
|
||||
case 'preces':
|
||||
if (part.omitOnDouble && isDoubleOrHigher(day.occurring)) {
|
||||
return [];
|
||||
}
|
||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||
case 'lesson':
|
||||
return [{ kind: 'lesson', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||
@@ -96,7 +49,7 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
}
|
||||
return [{ kind: 'lesson', text: resolveCommon('athanasian-creed'), label: 'Athanasian Creed' }];
|
||||
case 'closing-antiphon':
|
||||
return [{ kind: 'antiphon', text: splitWeekdayAntiphon(day.weekday).full }];
|
||||
return [{ kind: 'antiphon', text: splitNamedAntiphon(antiphons[day.weekday]).full }];
|
||||
case 'variable':
|
||||
if (part.resolve === 'by-season') {
|
||||
// Currently only the chapter responsory's verse uses this — see
|
||||
@@ -105,7 +58,7 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
}
|
||||
{
|
||||
const psalmRefs = getPsalmsFor('prime', day.weekday);
|
||||
const { incipit, full } = splitWeekdayAntiphon(day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(antiphons[day.weekday]);
|
||||
// Full text on a Double-or-higher feast, otherwise just the incipit
|
||||
// — see hours/antiphon.ts. The full repeat comes later, after the
|
||||
// Creed (see 'closing-antiphon'), not tacked onto the last psalm.
|
||||
@@ -130,8 +83,11 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
},
|
||||
];
|
||||
case 'canticle':
|
||||
return [{ kind: 'canticle', canticleId: part.canticleId }];
|
||||
return [{ kind: 'canticle', canticleId: part.canticleId, text: resolveCommon(part.textRef.id) }];
|
||||
case 'by-day-kind': {
|
||||
if (part.omitOnDouble && isDoubleOrHigher(day.occurring)) {
|
||||
return [];
|
||||
}
|
||||
const ref = isSundayOrFeast(day) ? part.sundayOrFeastRef : part.ferialRef;
|
||||
if (part.resolvedKind === 'preces') {
|
||||
return [{ kind: 'preces', text: resolveCommon(ref.id), label: part.label }];
|
||||
@@ -148,9 +104,13 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
return [
|
||||
{ kind: 'preces', text: resolveCommon('prime-regula-blessing') },
|
||||
{ kind: 'lesson', text: { text: reading.text, status: reading.status }, label },
|
||||
{ kind: 'preces', text: resolveCommon('prime-lesson-close') },
|
||||
{ kind: 'preces', text: resolveCommon('lesson-close') },
|
||||
];
|
||||
}
|
||||
// Not used by Prime — Compline-only.
|
||||
case 'nunc-dimittis':
|
||||
case 'marian-antiphon':
|
||||
throw new Error(`Prime's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user