compline: build the full hour, sharing Prime's resolver helpers
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:
2026-08-10 05:27:40 -04:00
parent 67037469fc
commit ec448bc35b
41 changed files with 1270 additions and 93 deletions
+58
View File
@@ -0,0 +1,58 @@
import type { ResolvedText } from './types';
import { getCommonProper } from '../propers';
import { splitAntiphon } from './antiphon';
/** Shared by every hour's resolver — looks up a common proper by id and
* shapes it as a ResolvedText. */
export function resolveCommon(id: string): ResolvedText {
const proper = getCommonProper(id);
return { text: proper.text, status: proper.status, citation: proper.citation };
}
export 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. */
export 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 bilingual 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. */
export function splitNamedAntiphon(text: Partial<Record<string, string>>): {
incipit: ResolvedText;
full: ResolvedText;
} {
const incipitText: Partial<Record<string, string>> = {};
const fullText: Partial<Record<string, string>> = {};
for (const [lang, t] of Object.entries(text)) {
if (!t) {
continue;
}
const split = splitAntiphon(t);
incipitText[lang] = `Ant. ${split.incipit}`;
fullText[lang] = `Ant. ${split.full}`;
}
return { incipit: verifiedText(incipitText), full: verifiedText(fullText) };
}