Wire the flexa mark into every hour's psalm-antiphon pairing
Deploy / deploy (push) Successful in 1m24s
Deploy / deploy (push) Successful in 1m24s
Every psalm's opening antiphon (Lauds, Prime, Terce, Sext, None, Vespers, Compline's callers, Matins) now runs through applyFlexaMark, marking the boundary in both the displayed antiphon and the psalm's first verse — e.g. today's Sunday Matins Nocturn 1: "Ant. The king rejoices ‡" / "The king rejoices ‡ in thy strength, O Lord...". Live-verified against the reference engine's own rendering for Ps 20. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCvvgFLrhXNVFUThz6kanw
This commit is contained in:
+11
-7
@@ -8,7 +8,7 @@ import { getCanticle } from './lauds-canticles';
|
||||
import { getLaudsPsalmodyOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollects,
|
||||
@@ -81,12 +81,16 @@ function capitulumId(weekday: Weekday): string {
|
||||
}
|
||||
|
||||
function psalmParts(numbers: number[], antiphon: BilingualText, opening: ResolvedText): ResolvedPart[] {
|
||||
const parts: ResolvedPart[] = numbers.map((number, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(number).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
const parts: ResolvedPart[] = numbers.map((number, i) => {
|
||||
const rawVerses = getPsalmVerses(number).map((v) => ({ n: v.n, text: v.text, status: v.status }));
|
||||
const { verses, antiphon: psalmAntiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: number,
|
||||
antiphon: psalmAntiphon,
|
||||
verses,
|
||||
};
|
||||
});
|
||||
parts.push({ kind: 'antiphon', text: splitNamedAntiphon(verifiedText(antiphon)).full });
|
||||
return parts;
|
||||
}
|
||||
|
||||
+16
-5
@@ -69,7 +69,7 @@ import { getPsalmVerses } from '../psalter';
|
||||
import { getPsalmsFor, type PsalmRef } from '../psalter/distribution';
|
||||
import { getScriptureVerses } from '../scripture';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
@@ -150,6 +150,15 @@ function plainPsalm(number: number): PsalmPart {
|
||||
};
|
||||
}
|
||||
|
||||
/** `plainPsalm` plus the given antiphon, with the flexa mark (see
|
||||
* hours/antiphon.ts's `applyFlexaMark`) applied to the first verse against
|
||||
* that antiphon — a no-op when `antiphon` is undefined. */
|
||||
function psalmPartWithAntiphon(number: number, rawAntiphon: ResolvedText | undefined): PsalmPart {
|
||||
const base = plainPsalm(number);
|
||||
const { verses, antiphon } = applyFlexaMark(base.verses, rawAntiphon);
|
||||
return { ...base, antiphon, verses };
|
||||
}
|
||||
|
||||
function psalmRefParts(refs: PsalmRef[]): ResolvedPart[] {
|
||||
return refs.map((ref) => ({
|
||||
kind: 'psalm' as const,
|
||||
@@ -167,7 +176,7 @@ function invitatoryParts(day: LiturgicalDay): ResolvedPart[] {
|
||||
const { incipit, full } = splitNamedAntiphon(resolveCommon('matins-invitatory-antiphon'));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
return [
|
||||
{ ...plainPsalm(94), antiphon: opening },
|
||||
psalmPartWithAntiphon(94, opening),
|
||||
{ kind: 'antiphon', text: full },
|
||||
];
|
||||
}
|
||||
@@ -181,7 +190,7 @@ function sundayPsalmNocturn(group: SundayNocturn, day: LiturgicalDay): ResolvedP
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(g.antiphon));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
g.psalms.forEach((n, i) => {
|
||||
parts.push({ ...plainPsalm(n), antiphon: i === 0 ? opening : undefined });
|
||||
parts.push(psalmPartWithAntiphon(n, i === 0 ? opening : undefined));
|
||||
});
|
||||
parts.push({ kind: 'antiphon', text: full });
|
||||
}
|
||||
@@ -282,11 +291,13 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon));
|
||||
const opening = isDouble ? full : incipit;
|
||||
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);
|
||||
parts.push({
|
||||
kind: 'psalm',
|
||||
psalmNumber: ref.number,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v): ResolvedVerse => ({ n: v.n, text: v.text, status: v.status })),
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses,
|
||||
antiphon,
|
||||
});
|
||||
});
|
||||
parts.push({ kind: 'antiphon', text: full });
|
||||
|
||||
+11
-7
@@ -5,7 +5,7 @@ import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
@@ -40,12 +40,16 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
const psalmRefs = getPsalmsFor('none', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('none', day, antiphons[day.weekday]));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
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);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon,
|
||||
verses,
|
||||
};
|
||||
});
|
||||
}
|
||||
// Not used by None.
|
||||
case 'prayer':
|
||||
|
||||
+11
-7
@@ -9,7 +9,7 @@ import { getRegulaReadingFor } from '../regula';
|
||||
import { getChapterResponsoryId } from './chapter-responsory';
|
||||
import { getHymnDoxologyId } from './hymn-doxology';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
appendDoxology,
|
||||
@@ -70,12 +70,16 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
// — 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;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
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);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon,
|
||||
verses,
|
||||
};
|
||||
});
|
||||
}
|
||||
case 'psalm':
|
||||
return [
|
||||
|
||||
+11
-7
@@ -5,7 +5,7 @@ import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
@@ -40,12 +40,16 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
const psalmRefs = getPsalmsFor('sext', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('sext', day, antiphons[day.weekday]));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
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);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon,
|
||||
verses,
|
||||
};
|
||||
});
|
||||
}
|
||||
// Not used by Sext.
|
||||
case 'prayer':
|
||||
|
||||
+11
-7
@@ -5,7 +5,7 @@ import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollect,
|
||||
@@ -40,12 +40,16 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
const psalmRefs = getPsalmsFor('terce', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(resolveMinorHourAntiphon('terce', day, antiphons[day.weekday]));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
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);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon,
|
||||
verses,
|
||||
};
|
||||
});
|
||||
}
|
||||
// Not used by Terce.
|
||||
case 'prayer':
|
||||
|
||||
+11
-7
@@ -4,7 +4,7 @@ import { resolveEveningDay } from '../calendar/vespers';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
||||
import {
|
||||
resolveCommon,
|
||||
getDayCollects,
|
||||
@@ -50,12 +50,16 @@ function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] {
|
||||
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon));
|
||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||
const refs = group.psalms.map(normalizePsalmRef);
|
||||
const parts: ResolvedPart[] = refs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
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 }));
|
||||
const { verses, antiphon } = applyFlexaMark(rawVerses, i === 0 ? opening : undefined);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon,
|
||||
verses,
|
||||
};
|
||||
});
|
||||
parts.push({ kind: 'antiphon', text: full });
|
||||
return parts;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveOrdo } from '../../src/hours';
|
||||
|
||||
describe('resolveOrdo("matins", ...) flexa mark on Ps 20', () => {
|
||||
it("marks where today's opening antiphon leaves off, in both the antiphon itself and Ps 20's first verse (Sunday Matins, Nocturn 1, 2026-08-23)", () => {
|
||||
const ordo = resolveOrdo('matins', '2026-08-23');
|
||||
const ps20 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 20);
|
||||
expect(ps20).toBeDefined();
|
||||
if (ps20?.kind !== 'psalm') throw new Error('expected a psalm part');
|
||||
const firstVerse = ps20.verses[0];
|
||||
expect(firstVerse?.text.la).toContain('‡');
|
||||
expect(firstVerse?.text.en).toContain('‡');
|
||||
expect(ps20.antiphon?.text.la).toContain('‡');
|
||||
expect(ps20.antiphon?.text.en).toContain('‡');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user