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:
+93
-4
@@ -1,6 +1,95 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
|
||||
import type { LiturgicalDay } from '../calendar/types';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getCommonProper } from '../propers';
|
||||
import { getHymnDoxologyId } from './hymn-doxology';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { resolveCommon, appendDoxology, splitNamedAntiphon } from './resolve-common';
|
||||
import complineDefinitionData from '../data/hours/compline.yml';
|
||||
|
||||
// Milestone 2. Not built yet.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'compline', date, parts: [], notImplemented: true };
|
||||
const complineDefinition = complineDefinitionData as HourDefinition;
|
||||
|
||||
// Real practice starts this at Lent proper (Ash Wednesday), not the
|
||||
// preceding Septuagesima-tide — unlike the opening versicle's Alleluia
|
||||
// suppression, which does start at Septuagesima (see
|
||||
// data/hours/opening-by-season.yml). Two different season keys deliberately
|
||||
// treated differently, not an oversight.
|
||||
const LENTEN_HYMN_SEASONS = new Set(['lent', 'passiontide']);
|
||||
|
||||
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'hymn': {
|
||||
// Lent replaces the whole hymn (not just its doxology) with "Christe,
|
||||
// qui lux es et dies" — self-contained, so no appendDoxology here.
|
||||
// See compline-hymn-lenten.yml for real caveats about how confident
|
||||
// to be that this actually triggers in live practice.
|
||||
if (LENTEN_HYMN_SEASONS.has(day.season)) {
|
||||
return [{ kind: 'hymn', text: resolveCommon('compline-hymn-lenten') }];
|
||||
}
|
||||
const body = resolveCommon(part.textRef.id);
|
||||
const doxology = resolveCommon(
|
||||
getHymnDoxologyId(day.season, day.occurring, 'compline-hymn-doxology-per-annum'),
|
||||
);
|
||||
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
|
||||
}
|
||||
case 'chapter':
|
||||
case 'responsory':
|
||||
case 'versicle':
|
||||
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 }];
|
||||
case 'opening-versicle':
|
||||
return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.occurring)) }];
|
||||
case 'psalm':
|
||||
return [
|
||||
{
|
||||
kind: 'psalm',
|
||||
psalmNumber: part.psalmNumber,
|
||||
verses: getPsalmVerses(part.psalmNumber, part.verses).map((v) => ({
|
||||
n: v.n,
|
||||
text: v.text,
|
||||
status: v.status,
|
||||
})),
|
||||
},
|
||||
];
|
||||
case 'nunc-dimittis': {
|
||||
const { incipit, full } = splitNamedAntiphon(getCommonProper('nunc-dimittis-antiphon').text);
|
||||
const opening = isDoubleOrHigher(day.occurring) ? full : incipit;
|
||||
return [
|
||||
{ kind: 'canticle', canticleId: 'nunc-dimittis', text: resolveCommon('nunc-dimittis'), antiphon: opening },
|
||||
{ kind: 'antiphon', text: full },
|
||||
];
|
||||
}
|
||||
case 'marian-antiphon': {
|
||||
const id = getMarianAntiphonId(day);
|
||||
return [{ kind: 'preces', text: resolveCommon(id), label: getMarianAntiphonLabel(id) }];
|
||||
}
|
||||
case 'canticle':
|
||||
return [{ kind: 'canticle', canticleId: part.canticleId, text: resolveCommon(part.textRef.id) }];
|
||||
// Not used by Compline — Prime-only, or milestone-4-only. Throwing
|
||||
// rather than silently dropping: if one of these ever shows up in
|
||||
// compline.yml, that's a mistake worth surfacing loudly, not hiding.
|
||||
case 'martyrology':
|
||||
case 'rule-reading':
|
||||
case 'creed':
|
||||
case 'closing-antiphon':
|
||||
case 'by-day-kind':
|
||||
case 'variable':
|
||||
throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
const day = resolveDay(date);
|
||||
const parts = complineDefinition.parts.flatMap((part) => resolvePart(part, day));
|
||||
return { hourId: 'compline', date, parts };
|
||||
}
|
||||
|
||||
@@ -3,10 +3,19 @@ import { resolveSeasonalPropersId } from './seasonal-propers';
|
||||
import bySeasonData from '../data/hours/hymn-doxology-by-season.yml';
|
||||
import byFeastData from '../data/hours/hymn-doxology-by-feast.yml';
|
||||
|
||||
const bySeason = bySeasonData as { perAnnum: string; bySeason: Record<string, string> };
|
||||
const bySeason = bySeasonData as { bySeason: Record<string, string> };
|
||||
const byFeast = byFeastData as { byFeastId: Record<string, string> };
|
||||
|
||||
/** Resolves the common-propers id for a hymn's seasonal final doxology stanza. */
|
||||
export function getHymnDoxologyId(season: Season, occurring: OccurringFeast[]): string {
|
||||
return resolveSeasonalPropersId(season, occurring, { ...bySeason, byFeastId: byFeast.byFeastId });
|
||||
/**
|
||||
* Resolves the common-propers id for a hymn's seasonal final doxology
|
||||
* stanza. The seasonal overrides are shared across every hymn (Divinum
|
||||
* Officium's own Doxologies.txt table is hymn-agnostic), but the per-annum
|
||||
* default is each hymn's own natural ending, so callers supply it.
|
||||
*/
|
||||
export function getHymnDoxologyId(season: Season, occurring: OccurringFeast[], perAnnumId: string): string {
|
||||
return resolveSeasonalPropersId(season, occurring, {
|
||||
perAnnum: perAnnumId,
|
||||
bySeason: bySeason.bySeason,
|
||||
byFeastId: byFeast.byFeastId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { LiturgicalDay } from '../calendar/types';
|
||||
import { easterSunday } from '../calendar/easter';
|
||||
import { addDays, toIsoDate } from '../calendar/date-math';
|
||||
import { resolveSeasonalPropersId } from './seasonal-propers';
|
||||
import bySeasonData from '../data/hours/marian-antiphon-by-season.yml';
|
||||
|
||||
const bySeason = bySeasonData as { perAnnum: string; bySeason: Record<string, string> };
|
||||
|
||||
const AVE_REGINA_CAELORUM_ID = 'marian-antiphon-ave-regina-caelorum';
|
||||
|
||||
/**
|
||||
* Ave Regina Caelorum's real window (Candlemas through Wednesday of Holy
|
||||
* Week) doesn't line up with any of the mutually-exclusive `season` values
|
||||
* the rest of the app uses (it starts mid-Epiphanytide and ends mid-Lent
|
||||
* or mid-Passiontide, depending on the year) — see calendar/types.ts's
|
||||
* Season doc comment for the general overlapping-windows problem this is
|
||||
* one instance of. Rather than force `season` to represent it, this checks
|
||||
* the real date directly, bypassing the by-season table for this one case.
|
||||
* The Triduum (Maundy Thursday through Holy Saturday) that follows isn't
|
||||
* specially handled — it falls through to whatever `season` resolves to
|
||||
* there (currently 'passiontide', not a key in marian-antiphon-by-season's
|
||||
* bySeason table, so it lands on the perAnnum default), which is a known,
|
||||
* separate gap, not something this fix claims to solve.
|
||||
*/
|
||||
function isCandlemasToHolyWednesday(isoDate: string): boolean {
|
||||
const year = Number(isoDate.slice(0, 4));
|
||||
const candlemas = `${year}-02-02`;
|
||||
const maundyThursday = addDays(toIsoDate(easterSunday(year)), -3);
|
||||
return isoDate >= candlemas && isoDate < maundyThursday;
|
||||
}
|
||||
|
||||
/** Resolves the common-propers id for Compline's closing Marian antiphon. */
|
||||
export function getMarianAntiphonId(day: LiturgicalDay): string {
|
||||
if (isCandlemasToHolyWednesday(day.date)) {
|
||||
return AVE_REGINA_CAELORUM_ID;
|
||||
}
|
||||
return resolveSeasonalPropersId(day.season, day.occurring, bySeason);
|
||||
}
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
'marian-antiphon-alma-redemptoris-advent': 'Alma Redemptoris Mater',
|
||||
'marian-antiphon-alma-redemptoris-nativity': 'Alma Redemptoris Mater',
|
||||
'marian-antiphon-ave-regina-caelorum': 'Ave Regina Caelorum',
|
||||
'marian-antiphon-regina-caeli': 'Regina Caeli',
|
||||
'marian-antiphon-salve-regina': 'Salve Regina',
|
||||
};
|
||||
|
||||
/** The real, singable name of whichever antiphon getMarianAntiphonId picked. */
|
||||
export function getMarianAntiphonLabel(id: string): string {
|
||||
return LABELS[id] ?? 'Marian Antiphon';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Season, OccurringFeast } from '../calendar/types';
|
||||
import { resolveSeasonalPropersId } from './seasonal-propers';
|
||||
import bySeasonData from '../data/hours/prime-opening-by-season.yml';
|
||||
import bySeasonData from '../data/hours/opening-by-season.yml';
|
||||
|
||||
const bySeason = bySeasonData as { perAnnum: string; bySeason: Record<string, string> };
|
||||
|
||||
|
||||
+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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
+17
-3
@@ -34,9 +34,12 @@ export type HourPart =
|
||||
// `label`, when given, is shown as a heading (e.g. "Preces", "Pretiosa") —
|
||||
// omitted for parts that are just connective tissue around a reading
|
||||
// (a blessing, a closing dialogue) that don't read as their own section.
|
||||
| { kind: 'preces'; textRef: PropersRef; label?: string }
|
||||
// `omitOnDouble` drops the whole part on a Double-or-higher feast (real
|
||||
// Preces are suppressed above a certain rank) — see calendar/
|
||||
// isDoubleOrHigher for how far that's actually wired up today.
|
||||
| { kind: 'preces'; textRef: PropersRef; label?: string; omitOnDouble?: true }
|
||||
| { kind: 'psalm'; psalmNumber: number; verses?: string; antiphonRef?: PropersRef }
|
||||
| { kind: 'canticle'; canticleId: string; antiphonRef?: PropersRef }
|
||||
| { kind: 'canticle'; canticleId: string; textRef: PropersRef; antiphonRef?: PropersRef }
|
||||
| {
|
||||
kind: 'lesson';
|
||||
textRef: PropersRef;
|
||||
@@ -57,6 +60,7 @@ export type HourPart =
|
||||
kind: 'by-day-kind';
|
||||
resolvedKind: 'chapter' | 'preces';
|
||||
label?: string;
|
||||
omitOnDouble?: true;
|
||||
sundayOrFeastRef: PropersRef;
|
||||
ferialRef: PropersRef;
|
||||
}
|
||||
@@ -72,6 +76,16 @@ export type HourPart =
|
||||
// in full here, distinct from the incipit-or-full opening attached to the
|
||||
// first psalm. See hours/antiphon.ts.
|
||||
| { kind: 'closing-antiphon' }
|
||||
// Compline's canticle — fixed year-round (not weekday-variable the way
|
||||
// Prime's psalm antiphon is), framed by "Salva nos" incipit-or-full
|
||||
// before and full after, same rank rule as any other antiphon. Resolves
|
||||
// to a 'canticle' ResolvedPart (with the opening antiphon inline) plus a
|
||||
// standalone 'antiphon' ResolvedPart for the closing repeat.
|
||||
| { kind: 'nunc-dimittis' }
|
||||
// Compline's closing Marian antiphon (with its own versicle and collect
|
||||
// baked into the same text block) — resolved by season, defaulting to
|
||||
// Salve Regina. See hours/marian-antiphon.ts.
|
||||
| { kind: 'marian-antiphon' }
|
||||
// Unused by Prime/Compline/the little hours — exercised starting milestone 4.
|
||||
| { kind: 'variable'; resolve: 'by-weekday' | 'by-season' | 'by-feast-rank' };
|
||||
|
||||
@@ -85,7 +99,7 @@ export type ResolvedPart =
|
||||
| { kind: 'hymn' | 'chapter' | 'responsory' | 'versicle' | 'prayer'; text: ResolvedText }
|
||||
| { kind: 'preces'; text: ResolvedText; label?: string }
|
||||
| { kind: 'psalm'; psalmNumber: number; antiphon?: ResolvedText; verses: ResolvedVerse[] }
|
||||
| { kind: 'canticle'; canticleId: string; antiphon?: ResolvedText }
|
||||
| { kind: 'canticle'; canticleId: string; text: ResolvedText; antiphon?: ResolvedText }
|
||||
// A standalone antiphon, said on its own rather than framing a psalm —
|
||||
// see 'closing-antiphon' above.
|
||||
| { kind: 'antiphon'; text: ResolvedText }
|
||||
|
||||
Reference in New Issue
Block a user