Compare commits
6 Commits
a6e4bb15b1
...
949196b847
| Author | SHA1 | Date | |
|---|---|---|---|
| 949196b847 | |||
| 74ddd61aee | |||
| f3cf30c1c7 | |||
| b163f502b8 | |||
| 7ab517cbe3 | |||
| a3a9d568ed |
@@ -0,0 +1,27 @@
|
|||||||
|
# Verified against Divinum Officium (Tridentine 1906/1910 and Divino
|
||||||
|
# Afflatu 1954, `[Ant Vespera]`), Latin/Sancti/08-29.txt +
|
||||||
|
# English/Sancti/08-29.txt, 2026-08-29 — Lauds reuses the Vespers set
|
||||||
|
# verbatim (`[Ant Laudes]` -> `@:Ant Vespera`).
|
||||||
|
id: decollation-of-st-john-baptist
|
||||||
|
groups:
|
||||||
|
- psalms: [92]
|
||||||
|
antiphon:
|
||||||
|
la: "Heródes enim ténuit * et ligávit Joánnem et pósuit in cárcerem propter Herodíadem."
|
||||||
|
en: "For Herod had laid hold upon John, * and bound him, and put him in prison, for Herodias' sake."
|
||||||
|
- psalms: [99]
|
||||||
|
antiphon:
|
||||||
|
la: "Dómine mi rex, * da mihi in disco caput Joánnis Baptístæ."
|
||||||
|
en: "O Lord my King, * give me in a charger the head of John the Baptist."
|
||||||
|
- psalms: [62]
|
||||||
|
antiphon:
|
||||||
|
la: "Puéllæ saltánti * imperávit mater: Nihil áliud petas, nisi caput Joánnis."
|
||||||
|
en: "The damsel danced, * and her mother charged her, saying: See thou ask nothing, but only the head of John."
|
||||||
|
canticle:
|
||||||
|
id: canticum-trium-puerorum
|
||||||
|
antiphon:
|
||||||
|
la: "Arguébat * Heródem Joánnes propter Herodíadem, quam túlerat fratri suo Philíppo uxórem."
|
||||||
|
en: "John had rebuked Herod for Herodias' sake, * his brother Philip's wife, for he had taken her."
|
||||||
|
laudate:
|
||||||
|
antiphon:
|
||||||
|
la: "Da mihi in disco * caput Joánnis Baptístæ. Et contristátus est rex propter jusjurándum."
|
||||||
|
en: "Give me in a charger the head of John the Baptist. * And the King was sorry, for his oath's sake."
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Verified against Divinum Officium (Tridentine 1906/1910 and Divino
|
||||||
|
# Afflatu 1954, `[Ant Vespera]`), Latin/Sancti/08-29.txt +
|
||||||
|
# English/Sancti/08-29.txt, 2026-08-29. The reference's own `[Rule]`
|
||||||
|
# ("Psalm5 Vespera=116") is a secular five-psalm-Vespers detail that
|
||||||
|
# doesn't apply to vu's Monastic-1617-structured (four-psalm) Vespers —
|
||||||
|
# same 109/111/112/115 baseline as common-of-a-martyr.yml's own Vespers
|
||||||
|
# psalmody, with this feast's own first four proper antiphons (of its
|
||||||
|
# five, `[Ant Vespera]` 1-4) substituted in; the fifth ("Da mihi in
|
||||||
|
# disco...") has no corresponding slot here and is used only at Lauds'
|
||||||
|
# Laudate psalm instead (see lauds-psalmody-overrides/decollation-of-st-
|
||||||
|
# john-baptist.yml).
|
||||||
|
id: decollation-of-st-john-baptist
|
||||||
|
groups:
|
||||||
|
- psalms: [109]
|
||||||
|
antiphon:
|
||||||
|
la: "Heródes enim ténuit * et ligávit Joánnem et pósuit in cárcerem propter Herodíadem."
|
||||||
|
en: "For Herod had laid hold upon John, * and bound him, and put him in prison, for Herodias' sake."
|
||||||
|
- psalms: [111]
|
||||||
|
antiphon:
|
||||||
|
la: "Dómine mi rex, * da mihi in disco caput Joánnis Baptístæ."
|
||||||
|
en: "O Lord my King, * give me in a charger the head of John the Baptist."
|
||||||
|
- psalms: [112]
|
||||||
|
antiphon:
|
||||||
|
la: "Puéllæ saltánti * imperávit mater: Nihil áliud petas, nisi caput Joánnis."
|
||||||
|
en: "The damsel danced, * and her mother charged her, saying: See thou ask nothing, but only the head of John."
|
||||||
|
- psalms: [115]
|
||||||
|
antiphon:
|
||||||
|
la: "Arguébat * Heródem Joánnes propter Herodíadem, quam túlerat fratri suo Philíppo uxórem."
|
||||||
|
en: "John had rebuked Herod for Herodias' sake, * his brother Philip's wife, for he had taken her."
|
||||||
+33
-16
@@ -4,7 +4,7 @@ import { resolveDay } from '../calendar';
|
|||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
import { getCanticle } from './lauds-canticles';
|
import { getCanticle } from './lauds-canticles';
|
||||||
import { getLaudsPsalmodyOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
import { getLaudsSaintOverride, getLaudsCommonOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||||
import { applyFlexaMark } from './antiphon';
|
import { applyFlexaMark } from './antiphon';
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
seasonalOfficeSuffix,
|
seasonalOfficeSuffix,
|
||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
getOfficeOverrideId,
|
getOfficeOverrideId,
|
||||||
|
getPsalmodyProperOverrideId,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
resolveOfficeBundle,
|
resolveOfficeBundle,
|
||||||
resolveSuffrages,
|
resolveSuffrages,
|
||||||
@@ -49,24 +50,40 @@ interface PsalmodyBlock {
|
|||||||
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
|
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A duplex-majus+ saint's own proper psalmody (per-feast, not per-Common —
|
* Two independent tiers, tried in order:
|
||||||
* explicit choice), or one of the named temporal feasts eligible via
|
|
||||||
* resolve-common.ts's getOfficeOverrideId. Falls back to `undefined` — the
|
|
||||||
* plain weekday default — for an eligible feast with no override authored
|
|
||||||
* yet, same honest incremental-content convention as everywhere else in
|
|
||||||
* this codebase; it's not gated behind whether content exists, just
|
|
||||||
* behind whether it's *eligible* to override at all.
|
|
||||||
*
|
*
|
||||||
* getOfficeOverrideId itself checks resolveOfficeWinner(day), not the raw
|
* 1. **Proper** — this feast's own authored antiphons (per-feast, not
|
||||||
* `day.winner` — on an octave day this is the octave's own feast (e.g. St.
|
* per-Common — explicit choice), eligible at *any rank* via
|
||||||
* Lawrence, days 2-8 of his own octave), which is exactly what should
|
* resolve-common.ts's getPsalmodyProperOverrideId. A saint's own real
|
||||||
* supply the psalmody override there too, live-verified alongside the
|
* proper text is never gated behind rank — see that function's own
|
||||||
* collect/Benedictus-antiphon fix (see resolve-common.ts's
|
* doc comment for why this used to be wrongly bundled with tier 2's
|
||||||
|
* gate (fixed 2026-08-29).
|
||||||
|
* 2. **Common category** — a generic-by-category substitute, still
|
||||||
|
* gated at duplex-majus+ via getOfficeOverrideId (a real, distinct,
|
||||||
|
* weaker tier: same conventional psalm numbers, not this feast's own
|
||||||
|
* text).
|
||||||
|
*
|
||||||
|
* Falls back to `undefined` — the plain weekday default — for an eligible
|
||||||
|
* feast with no override authored yet in either tier, same honest
|
||||||
|
* incremental-content convention as everywhere else in this codebase;
|
||||||
|
* it's not gated behind whether content exists, just behind whether it's
|
||||||
|
* *eligible* to override at all.
|
||||||
|
*
|
||||||
|
* Both id-eligibility functions check resolveOfficeWinner(day), not the
|
||||||
|
* raw `day.winner` — on an octave day this is the octave's own feast
|
||||||
|
* (e.g. St. Lawrence, days 2-8 of his own octave), which is exactly what
|
||||||
|
* should supply the psalmody override there too, live-verified alongside
|
||||||
|
* the collect/Benedictus-antiphon fix (see resolve-common.ts's
|
||||||
* resolveOfficeWinner doc comment).
|
* resolveOfficeWinner doc comment).
|
||||||
*/
|
*/
|
||||||
function getPsalmodyOverrideFor(day: LiturgicalDay): LaudsPsalmodyOverride | undefined {
|
function getPsalmodyOverrideFor(day: LiturgicalDay): LaudsPsalmodyOverride | undefined {
|
||||||
const id = getOfficeOverrideId(day);
|
const properId = getPsalmodyProperOverrideId(day);
|
||||||
return id ? getLaudsPsalmodyOverride(id) : undefined;
|
const proper = properId ? getLaudsSaintOverride(properId) : undefined;
|
||||||
|
if (proper) {
|
||||||
|
return proper;
|
||||||
|
}
|
||||||
|
const categoryId = getOfficeOverrideId(day);
|
||||||
|
return categoryId ? getLaudsCommonOverride(categoryId) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mon-Fri share one capitulum verbatim (confirmed against the live engine
|
// Mon-Fri share one capitulum verbatim (confirmed against the live engine
|
||||||
@@ -128,7 +145,7 @@ function canticleText(canticleId: string, slice?: [number, number]): ResolvedTex
|
|||||||
}
|
}
|
||||||
status[lang] = worst;
|
status[lang] = worst;
|
||||||
}
|
}
|
||||||
return { text, status };
|
return { text, status, citation: { la: canticle.citation, en: canticle.citation } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ps 66 through the Laudate psalms — see hours/types.ts's 'lauds-psalmody'
|
/** Ps 66 through the Laudate psalms — see hours/types.ts's 'lauds-psalmody'
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, is
|
|||||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||||
import { isInTriduum } from '../calendar/temporal';
|
import { isInTriduum } from '../calendar/temporal';
|
||||||
import { splitAntiphon, isDoubleOrHigher } from './antiphon';
|
import { splitAntiphon, isDoubleOrHigher } from './antiphon';
|
||||||
|
import { getLaudsSaintOverride } from './lauds-psalmody-overrides';
|
||||||
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
||||||
|
|
||||||
type BilingualText = Partial<Record<string, string>>;
|
type BilingualText = Partial<Record<string, string>>;
|
||||||
@@ -180,6 +181,34 @@ export function getOfficeOverrideId(day: LiturgicalDay): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id to look up for today's winner's own *proper* Lauds/Vespers
|
||||||
|
* psalmody antiphons (`hours/lauds.ts`'s and `hours/vespers.ts`'s own
|
||||||
|
* `getPsalmodyOverrideFor`) — eligible at **any rank**, unlike
|
||||||
|
* `getOfficeOverrideId` above. A saint's own authored proper antiphon
|
||||||
|
* text isn't something rank should ever gate — rank only has standing to
|
||||||
|
* gate the weaker, generic Common-category substitute (still looked up
|
||||||
|
* via `getOfficeOverrideId`'s duplex-majus+ threshold, a real distinct
|
||||||
|
* tier: same conventional psalm numbers, a generic-by-category antiphon,
|
||||||
|
* not this feast's own text). Mirrors `getMinorHourOverrideId`'s identical
|
||||||
|
* "any sanctoral winner is eligible, it's the caller that decides whether
|
||||||
|
* anything's actually authored for the id" reasoning — the psalmody
|
||||||
|
* override table previously reused `getOfficeOverrideId`'s single
|
||||||
|
* duplex-majus+ gate for both its proper and Common tiers at once, which
|
||||||
|
* silently discarded a proper antiphon whenever it happened to be
|
||||||
|
* authored for a sub-duplex-majus feast (fixed 2026-08-29).
|
||||||
|
*/
|
||||||
|
export function getPsalmodyProperOverrideId(day: LiturgicalDay): string | undefined {
|
||||||
|
const winner = resolveOfficeWinner(day);
|
||||||
|
if (winner.kind === 'temporal' && ALWAYS_OVERRIDE_TEMPORAL_IDS.has(winner.id)) {
|
||||||
|
return winner.id;
|
||||||
|
}
|
||||||
|
if (winner.kind === 'sanctoral') {
|
||||||
|
return winner.id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id to look up for today's office winner's (resolveOfficeWinner)
|
* The id to look up for today's office winner's (resolveOfficeWinner)
|
||||||
* minor-hour content (antiphon + chapter) — `${hourId}-antiphon-${id}`/
|
* minor-hour content (antiphon + chapter) — `${hourId}-antiphon-${id}`/
|
||||||
@@ -328,19 +357,52 @@ function withPaschaltideAlleluia(resolved: ResolvedText, day: LiturgicalDay): Re
|
|||||||
return appendPaschaltideAlleluia(resolved);
|
return appendPaschaltideAlleluia(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The reference engine's own `getanthoras()` (specials.pl): when a saint
|
||||||
|
// has no dedicated per-hour minor-hour antiphon of its own, one is
|
||||||
|
// derived from that saint's 5-antiphon Lauds/Vespers set (`[Ant Laudes]`,
|
||||||
|
// which for most saints is `@:Ant Vespera`) by fixed index — Prime takes
|
||||||
|
// the first (the same antiphon as the first psalm-group), Terce/Sext the
|
||||||
|
// second/third, None the fifth (the Laudate antiphon); the fourth
|
||||||
|
// (canticle) antiphon is never used for a minor hour. vu's own
|
||||||
|
// `LaudsPsalmodyOverride` already stores that same 5-antiphon set in that
|
||||||
|
// exact order (`groups[0..2]`, `canticle`, `laudate`), so once a saint's
|
||||||
|
// Lauds proper is authored, no separate per-hour file is needed for this
|
||||||
|
// tier — same "mechanism first, content incrementally" pattern as
|
||||||
|
// everywhere else in this codebase.
|
||||||
|
const MINOR_HOUR_LAUDS_ANTIPHON_INDEX: Record<string, number> = { prime: 0, terce: 1, sext: 2, none: 4 };
|
||||||
|
|
||||||
|
function deriveMinorHourAntiphonFromLauds(hourId: string, overrideId: string): ResolvedText | undefined {
|
||||||
|
const index = MINOR_HOUR_LAUDS_ANTIPHON_INDEX[hourId];
|
||||||
|
if (index === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const lauds = getLaudsSaintOverride(overrideId);
|
||||||
|
if (!lauds) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const antiphon = index === 4 ? lauds.laudate.antiphon : lauds.groups[index]?.antiphon;
|
||||||
|
if (!antiphon) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return verifiedText(antiphon);
|
||||||
|
}
|
||||||
|
|
||||||
/** A Little Hour's (or Prime's) plain per-weekday antiphon, overridden by
|
/** A Little Hour's (or Prime's) plain per-weekday antiphon, overridden by
|
||||||
* a duplex-majus+ feast's own proper (`${hourId}-antiphon-${id}`) when
|
* a duplex-majus+ feast's own proper (`${hourId}-antiphon-${id}`) when
|
||||||
* authored, then by that saint's shared Common (`${hourId}-antiphon-
|
* authored, then by that saint's own Lauds/Vespers antiphon set (see
|
||||||
|
* `deriveMinorHourAntiphonFromLauds` above — a real liturgical derivation,
|
||||||
|
* not a guess), then by that saint's shared Common (`${hourId}-antiphon-
|
||||||
* ${minorHoursCommon}`, see SaintRecord's own doc comment) — tried first
|
* ${minorHoursCommon}`, see SaintRecord's own doc comment) — tried first
|
||||||
* in its Paschaltide-variant form (`${minorHoursCommon}-paschaltide`)
|
* in its Paschaltide-variant form (`${minorHoursCommon}-paschaltide`)
|
||||||
* when the day falls in Paschaltide and that variant has been authored,
|
* when the day falls in Paschaltide and that variant has been authored,
|
||||||
* per PASCHALTIDE_SEASONS's own doc comment — when a proper one hasn't
|
* per PASCHALTIDE_SEASONS's own doc comment — when neither of the first
|
||||||
* been authored — same honest "not gated behind whether content exists,
|
* two has been authored — same honest "not gated behind whether content
|
||||||
* just eligible to override at all" fallback as every other override in
|
* exists, just eligible to override at all" fallback as every other
|
||||||
* this codebase: an eligible feast with none of these authored yet just
|
* override in this codebase: an eligible feast with none of these
|
||||||
* falls through to the plain weekday default silently. The plain (non-
|
* authored yet just falls through to the plain weekday default silently.
|
||||||
* `-paschaltide`) Common fallback itself still picks up a seasonal change
|
* The plain (non-`-paschaltide`) Common fallback itself still picks up a
|
||||||
* in Paschaltide — see `withPaschaltideAlleluia`.
|
* seasonal change in Paschaltide — see `withPaschaltideAlleluia`. (The
|
||||||
|
* derived-from-Lauds tier does not get that treatment — see TODO.md.)
|
||||||
*/
|
*/
|
||||||
export function resolveMinorHourAntiphon(
|
export function resolveMinorHourAntiphon(
|
||||||
hourId: string,
|
hourId: string,
|
||||||
@@ -353,6 +415,10 @@ export function resolveMinorHourAntiphon(
|
|||||||
if (proper.status.la !== 'missing' || proper.status.en !== 'missing') {
|
if (proper.status.la !== 'missing' || proper.status.en !== 'missing') {
|
||||||
return proper;
|
return proper;
|
||||||
}
|
}
|
||||||
|
const derived = deriveMinorHourAntiphonFromLauds(hourId, overrideId);
|
||||||
|
if (derived) {
|
||||||
|
return derived;
|
||||||
|
}
|
||||||
const commonId = getSaintRecord(overrideId)?.minorHoursCommon;
|
const commonId = getSaintRecord(overrideId)?.minorHoursCommon;
|
||||||
if (commonId) {
|
if (commonId) {
|
||||||
if (PASCHALTIDE_SEASONS.has(day.season)) {
|
if (PASCHALTIDE_SEASONS.has(day.season)) {
|
||||||
@@ -542,7 +608,16 @@ function sanctoralCommemorationPart(commemoration: Extract<Commemoration, { kind
|
|||||||
if (combined.status.la !== 'missing' || combined.status.en !== 'missing') {
|
if (combined.status.la !== 'missing' || combined.status.en !== 'missing') {
|
||||||
return { kind: 'preces', text: combined, label };
|
return { kind: 'preces', text: combined, label };
|
||||||
}
|
}
|
||||||
return { kind: 'preces', text: resolveCommon(`${saint.propers}-collect`), label };
|
const properCollect = resolveCommon(`${saint.propers}-collect`);
|
||||||
|
if (properCollect.status.la !== 'missing' || properCollect.status.en !== 'missing') {
|
||||||
|
return { kind: 'preces', text: properCollect, label };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (saint?.collectCommon && saint.collectName) {
|
||||||
|
const template = resolveCommon(saint.collectCommon);
|
||||||
|
if (template.status.la !== 'missing' || template.status.en !== 'missing') {
|
||||||
|
return { kind: 'preces', text: substituteName(template, saint.collectName), label };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { kind: 'preces', text: { text: {}, status: { la: 'missing', en: 'missing' } }, label };
|
return { kind: 'preces', text: { text: {}, status: { la: 'missing', en: 'missing' } }, label };
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-14
@@ -5,7 +5,7 @@ import { weekdayOf } from '../calendar/weekday';
|
|||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { getVespersPsalmodyOverride } from './vespers-psalmody-overrides';
|
import { getVespersSaintOverride, getVespersCommonOverride } from './vespers-psalmody-overrides';
|
||||||
import { applyFlexaMark } from './antiphon';
|
import { applyFlexaMark } from './antiphon';
|
||||||
import {
|
import {
|
||||||
resolveCommon,
|
resolveCommon,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
seasonalOfficeSuffix,
|
seasonalOfficeSuffix,
|
||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
getOfficeOverrideId,
|
getOfficeOverrideId,
|
||||||
|
getPsalmodyProperOverrideId,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
resolveOfficeBundle,
|
resolveOfficeBundle,
|
||||||
resolveSuffrages,
|
resolveSuffrages,
|
||||||
@@ -70,20 +71,31 @@ function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A duplex-majus+ saint's own proper Vespers psalmody (none authored
|
/** Two independent tiers, tried in order — same split as hours/lauds.ts's
|
||||||
* yet), or their Common's (live-verified 2026-08-28, see
|
* own getPsalmodyOverrideFor, see its doc comment for the full rationale:
|
||||||
* hours/vespers-psalmody-overrides.ts's own doc comment for why this
|
*
|
||||||
* mechanism is genuinely per-Common — unlike Lauds, the psalm *numbers*
|
* 1. **Proper** — this feast's own authored psalmody, eligible at *any
|
||||||
* themselves differ by Common here, not just the antiphons). Eligibility
|
* rank* via resolve-common.ts's getPsalmodyProperOverrideId (a saint's
|
||||||
* follows the evening's governing `day` (same getOfficeOverrideId test
|
* own real proper text is never gated behind rank).
|
||||||
* as the chapter/responsory/hymn/versicle bundle below) — a First
|
* 2. **Common category** — per hours/vespers-psalmody-overrides.ts's own
|
||||||
* Vespers of a majus+ feast should get that feast's own psalms, same as
|
* doc comment, genuinely per-Common here (the psalm *numbers*
|
||||||
* it gets that feast's own office. Falls back to `undefined` for an
|
* themselves differ by Common, not just the antiphons, unlike Lauds)
|
||||||
* eligible feast with no override authored yet, same honest incremental-
|
* — still gated at duplex-majus+ via getOfficeOverrideId.
|
||||||
* content convention as everywhere else. */
|
*
|
||||||
|
* Eligibility follows the evening's governing `day` (same
|
||||||
|
* resolveOfficeWinner-based test as the chapter/responsory/hymn/versicle
|
||||||
|
* bundle below) — a First Vespers of a feast should get that feast's own
|
||||||
|
* psalms, same as it gets that feast's own office. Falls back to
|
||||||
|
* `undefined` for an eligible feast with no override authored in either
|
||||||
|
* tier, same honest incremental-content convention as everywhere else. */
|
||||||
function getPsalmodyOverrideFor(day: LiturgicalDay): VespersDay | undefined {
|
function getPsalmodyOverrideFor(day: LiturgicalDay): VespersDay | undefined {
|
||||||
const id = getOfficeOverrideId(day);
|
const properId = getPsalmodyProperOverrideId(day);
|
||||||
return id ? getVespersPsalmodyOverride(id) : undefined;
|
const proper = properId ? getVespersSaintOverride(properId) : undefined;
|
||||||
|
if (proper) {
|
||||||
|
return proper;
|
||||||
|
}
|
||||||
|
const categoryId = getOfficeOverrideId(day);
|
||||||
|
return categoryId ? getVespersCommonOverride(categoryId) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The 4 weekday-variable psalm groups — no fixed leading/trailing psalm
|
/** The 4 weekday-variable psalm groups — no fixed leading/trailing psalm
|
||||||
|
|||||||
+13
-7
@@ -26,12 +26,18 @@ function renderColumns(resolved: ResolvedText | ResolvedVerse, languages: readon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scripture quotations always carry a full citation (book, chapter, verse) —
|
// Scripture quotations always carry a full citation (book, chapter, verse) —
|
||||||
// shown once, deduplicated across languages when they happen to match.
|
// shown once per currently-displayed language, deduplicated when they
|
||||||
function renderCitation(text: ResolvedText): string {
|
// happen to match (e.g. a plain "Psalm 118" citation reused verbatim in
|
||||||
|
// both columns) — restricted to `languages` so a language-specific
|
||||||
|
// abbreviation (Latin "Jac 1:12" vs. English "Jas 1:12") never leaks into
|
||||||
|
// a single-language view.
|
||||||
|
function renderCitation(text: ResolvedText, languages: readonly string[]): string {
|
||||||
if (!text.citation) {
|
if (!text.citation) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const values = [...new Set(Object.values(text.citation).filter((v): v is string => Boolean(v)))];
|
const values = [
|
||||||
|
...new Set(languages.map((lang) => text.citation![lang]).filter((v): v is string => Boolean(v))),
|
||||||
|
];
|
||||||
return values.length ? `<p class="ordo-part-citation">${escapeHtml(values.join(' / '))}</p>` : '';
|
return values.length ? `<p class="ordo-part-citation">${escapeHtml(values.join(' / '))}</p>` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +70,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
|||||||
return `
|
return `
|
||||||
<section class="ordo-part ordo-part-${part.kind}">
|
<section class="ordo-part ordo-part-${part.kind}">
|
||||||
<h3 class="ordo-part-label">${hourLabel(part.kind)}</h3>
|
<h3 class="ordo-part-label">${hourLabel(part.kind)}</h3>
|
||||||
${renderCitation(part.text)}
|
${renderCitation(part.text, languages)}
|
||||||
${renderColumns(part.text, languages)}
|
${renderColumns(part.text, languages)}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@@ -93,7 +99,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
|||||||
return `
|
return `
|
||||||
<section class="ordo-part ordo-part-lesson">
|
<section class="ordo-part ordo-part-lesson">
|
||||||
${renderLessonLabel(part.label, languages)}
|
${renderLessonLabel(part.label, languages)}
|
||||||
${renderCitation(part.text)}
|
${renderCitation(part.text, languages)}
|
||||||
${renderColumns(part.text, languages)}
|
${renderColumns(part.text, languages)}
|
||||||
${part.responsory ? `<div class="ordo-part-responsory">${renderColumns(part.responsory, languages)}</div>` : ''}
|
${part.responsory ? `<div class="ordo-part-responsory">${renderColumns(part.responsory, languages)}</div>` : ''}
|
||||||
</section>
|
</section>
|
||||||
@@ -110,7 +116,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
|||||||
return `
|
return `
|
||||||
<section class="ordo-part ordo-part-lesson ordo-part-gospel">
|
<section class="ordo-part ordo-part-lesson ordo-part-gospel">
|
||||||
<h3 class="ordo-part-label">Gospel</h3>
|
<h3 class="ordo-part-label">Gospel</h3>
|
||||||
${renderCitation(part.text)}
|
${renderCitation(part.text, languages)}
|
||||||
${renderColumns(part.text, languages)}
|
${renderColumns(part.text, languages)}
|
||||||
${part.source ? `<p class="ordo-part-citation">${escapeHtml(part.source)}</p>` : ''}
|
${part.source ? `<p class="ordo-part-citation">${escapeHtml(part.source)}</p>` : ''}
|
||||||
${part.responsory ? `<div class="ordo-part-responsory">${renderColumns(part.responsory, languages)}</div>` : ''}
|
${part.responsory ? `<div class="ordo-part-responsory">${renderColumns(part.responsory, languages)}</div>` : ''}
|
||||||
@@ -170,7 +176,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
|||||||
<section class="ordo-part ordo-part-canticle">
|
<section class="ordo-part ordo-part-canticle">
|
||||||
${part.antiphon ? renderColumns(part.antiphon, languages) : ''}
|
${part.antiphon ? renderColumns(part.antiphon, languages) : ''}
|
||||||
<h3 class="ordo-part-label lang-columns lang-columns-${languages.length}">${titleCols}</h3>
|
<h3 class="ordo-part-label lang-columns lang-columns-${languages.length}">${titleCols}</h3>
|
||||||
${renderCitation(part.text)}
|
${renderCitation(part.text, languages)}
|
||||||
${renderColumns(part.text, languages)}
|
${renderColumns(part.text, languages)}
|
||||||
${part.gloriaPatri ? `<div class="ordo-part-gloria-patri">${renderColumns(part.gloriaPatri, languages)}</div>` : ''}
|
${part.gloriaPatri ? `<div class="ordo-part-gloria-patri">${renderColumns(part.gloriaPatri, languages)}</div>` : ''}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user