Centralize antiphon opening-doubling logic across all hours
The same idiom (splitNamedAntiphon + isDoubleOrHigher(...) ? full : incipit) was copy-pasted at 11 call sites across 8 hour files. Add one shared openingAntiphon(antiphon, winner) helper in resolve-common.ts and use it everywhere a psalm/canticle antiphon's opening form is decided. Pure dedup, no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
This commit is contained in:
@@ -7,7 +7,7 @@ import { getHymnDoxologyId } from './hymn-doxology';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { resolveCommon, appendDoxology, splitNamedAntiphon, resolveResponsory } from './resolve-common';
|
||||
import { resolveCommon, appendDoxology, splitNamedAntiphon, openingAntiphon, resolveResponsory } from './resolve-common';
|
||||
import complineDefinitionData from '../data/hours/compline.yml';
|
||||
|
||||
const complineDefinition = complineDefinitionData as HourDefinition;
|
||||
@@ -63,8 +63,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
},
|
||||
];
|
||||
case 'nunc-dimittis': {
|
||||
const { incipit, full } = splitNamedAntiphon(resolveCommon('nunc-dimittis-antiphon'));
|
||||
const opening = isDoubleOrHigher(day.winner) ? full : incipit;
|
||||
const antiphonText = resolveCommon('nunc-dimittis-antiphon');
|
||||
const opening = openingAntiphon(antiphonText, day.winner);
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
return [
|
||||
{ kind: 'canticle', canticleId: 'nunc-dimittis', text: resolveCommon('nunc-dimittis'), antiphon: opening },
|
||||
{ kind: 'antiphon', text: full },
|
||||
|
||||
+6
-6
@@ -14,6 +14,7 @@ import {
|
||||
getDayCollects,
|
||||
getBenedictusAntiphon,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
resolveOfficeWinner,
|
||||
verifiedText,
|
||||
seasonalOfficeSuffix,
|
||||
@@ -134,10 +135,8 @@ function canticleText(canticleId: string, slice?: [number, number]): ResolvedTex
|
||||
* plain weekday default below gets substituted. */
|
||||
function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
||||
const wd = getPsalmodyOverrideFor(day) ?? laudsAntiphons[day.weekday];
|
||||
const opening = (antiphon: BilingualText) => {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(antiphon));
|
||||
return isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
};
|
||||
const winner = resolveOfficeWinner(day);
|
||||
const opening = (antiphon: BilingualText) => openingAntiphon(verifiedText(antiphon), winner);
|
||||
const parts: ResolvedPart[] = [
|
||||
{
|
||||
kind: 'psalm',
|
||||
@@ -237,8 +236,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
case 'lauds-office':
|
||||
return resolveOffice(day);
|
||||
case 'benedictus': {
|
||||
const { incipit, full } = splitNamedAntiphon(getBenedictusAntiphon(day));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const antiphonText = getBenedictusAntiphon(day);
|
||||
const opening = openingAntiphon(antiphonText, resolveOfficeWinner(day));
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
return [
|
||||
{ kind: 'canticle', canticleId: 'benedictus', text: resolveCommon('benedictus'), antiphon: opening },
|
||||
{ kind: 'antiphon', text: full },
|
||||
|
||||
+11
-7
@@ -78,6 +78,7 @@ import {
|
||||
seasonalOfficeSuffix,
|
||||
verifiedText,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
} from './resolve-common';
|
||||
import { getBiblePlanReadings } from '../propers/bible-plan';
|
||||
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
|
||||
@@ -187,8 +188,9 @@ function invitatoryParts(day: LiturgicalDay): ResolvedPart[] {
|
||||
function sundayPsalmNocturn(group: SundayNocturn, day: LiturgicalDay): ResolvedPart[] {
|
||||
const parts: ResolvedPart[] = [];
|
||||
for (const g of group.groups ?? []) {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(g.antiphon));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const antiphonText = verifiedText(g.antiphon);
|
||||
const opening = openingAntiphon(antiphonText, resolveOfficeWinner(day));
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
g.psalms.forEach((n, i) => {
|
||||
parts.push(psalmPartWithAntiphon(n, i === 0 ? opening : undefined));
|
||||
});
|
||||
@@ -201,8 +203,9 @@ function sundayPsalmNocturn(group: SundayNocturn, day: LiturgicalDay): ResolvedP
|
||||
/** Nocturn 3's 3 fixed OT canticles under one shared antiphon — see
|
||||
* data/hours/matins-sunday-antiphons.yml's own header. */
|
||||
function sundayCanticleNocturn(group: SundayNocturn, day: LiturgicalDay): ResolvedPart[] {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon ?? {}));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const antiphonText = verifiedText(group.antiphon ?? {});
|
||||
const opening = openingAntiphon(antiphonText, resolveOfficeWinner(day));
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
const parts: ResolvedPart[] = (group.canticles ?? []).map((c, i) => {
|
||||
const verses = c.refs.flatMap((ref) => getScriptureVerses(ref.book, ref.chapter, ref.verses));
|
||||
const text: BilingualText = {
|
||||
@@ -285,11 +288,12 @@ function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
|
||||
* as one combined V./R. block via `versicleText`. */
|
||||
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
||||
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
||||
const isDouble = isDoubleOrHigher(resolveOfficeWinner(day));
|
||||
const winner = resolveOfficeWinner(day);
|
||||
const parts: ResolvedPart[] = [];
|
||||
for (const group of nocturn.groups) {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon));
|
||||
const opening = isDouble ? full : incipit;
|
||||
const antiphonText = verifiedText(group.antiphon);
|
||||
const opening = openingAntiphon(antiphonText, winner);
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
group.psalms.forEach((ref, i) => {
|
||||
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v): ResolvedVerse => ({ n: v.n, text: v.text, status: v.status }));
|
||||
const { verses, antiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
|
||||
+3
-4
@@ -5,11 +5,11 @@ import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import { applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
resolveOfficeWinner,
|
||||
resolveMinorHourAntiphon,
|
||||
resolveMinorHourChapter,
|
||||
@@ -38,8 +38,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
// psalms, no closing repeat (unlike Prime): confirmed directly
|
||||
// against the engine, Capitulum follows the third psalm immediately.
|
||||
const psalmRefs = getPsalmsFor('none', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('none', day, antiphons[day.weekday]));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const opening = openingAntiphon(resolveMinorHourAntiphon('none', day, antiphons[day.weekday]), resolveOfficeWinner(day));
|
||||
return psalmRefs.map((ref, i) => {
|
||||
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status }));
|
||||
const { verses, antiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
|
||||
+3
-2
@@ -14,6 +14,7 @@ import {
|
||||
resolveCommon,
|
||||
appendDoxology,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
resolveOfficeWinner,
|
||||
resolveMinorHourAntiphon,
|
||||
isSundayOrFeastOffice,
|
||||
@@ -65,11 +66,11 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
}
|
||||
{
|
||||
const psalmRefs = getPsalmsFor('prime', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('prime', day, antiphons[day.weekday]));
|
||||
const antiphonText = resolveMinorHourAntiphon('prime', day, 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.
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const opening = openingAntiphon(antiphonText, resolveOfficeWinner(day));
|
||||
return psalmRefs.map((ref, i) => {
|
||||
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status }));
|
||||
const { verses, antiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getSaintRecord } from '../calendar/feasts';
|
||||
import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, isAtLeast } from '../calendar';
|
||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||
import { isInTriduum } from '../calendar/temporal';
|
||||
import { splitAntiphon } from './antiphon';
|
||||
import { splitAntiphon, isDoubleOrHigher } from './antiphon';
|
||||
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
||||
|
||||
type BilingualText = Partial<Record<string, string>>;
|
||||
@@ -692,6 +692,19 @@ export function splitNamedAntiphon(antiphon: ResolvedText): {
|
||||
return { incipit: { text: incipitText, status: incipitStatus }, full: { text: fullText, status: fullStatus } };
|
||||
}
|
||||
|
||||
/** The antiphon actually shown *before* a psalm/canticle: full text on a
|
||||
* Double-rank winner or higher, incipit only below that (see
|
||||
* `isDoubleOrHigher`'s own doc comment for the underlying rule) — the one
|
||||
* idiom every hour's psalmody independently repeated (`splitNamedAntiphon`
|
||||
* + `isDoubleOrHigher(...) ? full : incipit`) before being centralized
|
||||
* here. The closing repeat after the psalm/canticle is always `full`,
|
||||
* unconditionally, at each call site — this helper only decides the
|
||||
* opening. */
|
||||
export function openingAntiphon(antiphon: ResolvedText, winner: DayWinner): ResolvedText {
|
||||
const { incipit, full } = splitNamedAntiphon(antiphon);
|
||||
return isDoubleOrHigher(winner) ? full : incipit;
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
+3
-4
@@ -5,11 +5,11 @@ import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import { applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
resolveOfficeWinner,
|
||||
resolveMinorHourAntiphon,
|
||||
resolveMinorHourChapter,
|
||||
@@ -38,8 +38,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
// psalms, no closing repeat (unlike Prime): confirmed directly
|
||||
// against the engine, Capitulum follows the third psalm immediately.
|
||||
const psalmRefs = getPsalmsFor('sext', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('sext', day, antiphons[day.weekday]));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const opening = openingAntiphon(resolveMinorHourAntiphon('sext', day, antiphons[day.weekday]), resolveOfficeWinner(day));
|
||||
return psalmRefs.map((ref, i) => {
|
||||
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status }));
|
||||
const { verses, antiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
|
||||
+3
-4
@@ -5,11 +5,11 @@ import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import { applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
resolveOfficeWinner,
|
||||
resolveMinorHourAntiphon,
|
||||
resolveMinorHourChapter,
|
||||
@@ -38,8 +38,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
// psalms, no closing repeat (unlike Prime): confirmed directly
|
||||
// against the engine, Capitulum follows the third psalm immediately.
|
||||
const psalmRefs = getPsalmsFor('terce', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('terce', day, antiphons[day.weekday]));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const opening = openingAntiphon(resolveMinorHourAntiphon('terce', day, antiphons[day.weekday]), resolveOfficeWinner(day));
|
||||
return psalmRefs.map((ref, i) => {
|
||||
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status }));
|
||||
const { verses, antiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
|
||||
@@ -4,12 +4,13 @@ import { resolveEveningDay } from '../calendar/vespers';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import { applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollects,
|
||||
getMagnificatAntiphon,
|
||||
splitNamedAntiphon,
|
||||
openingAntiphon,
|
||||
resolveOfficeWinner,
|
||||
verifiedText,
|
||||
seasonalOfficeSuffix,
|
||||
@@ -47,8 +48,9 @@ function normalizePsalmRef(ref: PsalmRefLike): { number: number; verses?: string
|
||||
* in full once, after the last. Mirrors lauds.ts's psalmParts, extended
|
||||
* for verse-range entries (see PsalmRefLike). */
|
||||
function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const antiphonText = verifiedText(group.antiphon);
|
||||
const opening = openingAntiphon(antiphonText, resolveOfficeWinner(day));
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
const refs = group.psalms.map(normalizePsalmRef);
|
||||
const parts: ResolvedPart[] = refs.map((ref, i) => {
|
||||
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status }));
|
||||
@@ -161,8 +163,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
case 'vespers-office':
|
||||
return resolveOffice(day);
|
||||
case 'magnificat': {
|
||||
const { incipit, full } = splitNamedAntiphon(getMagnificatAntiphon(day));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const antiphonText = getMagnificatAntiphon(day);
|
||||
const opening = openingAntiphon(antiphonText, resolveOfficeWinner(day));
|
||||
const { full } = splitNamedAntiphon(antiphonText);
|
||||
return [
|
||||
{ kind: 'canticle', canticleId: 'magnificat', text: resolveCommon('magnificat'), antiphon: opening },
|
||||
{ kind: 'antiphon', text: full },
|
||||
|
||||
Reference in New Issue
Block a user