Add antiphons and versicle to plain ferial weekday Matins nocturns
Deploy / deploy (push) Successful in 1m13s
Deploy / deploy (push) Successful in 1m13s
Every non-Sunday, non-Duplex+ Matins nocturn rendered bare psalms with no antiphon at all. Sourced each psalm's antiphon from Divinum Officium's Monastic Matins psalter and regrouped to fit psalter-distribution.yml's own already-established (and non-historical) weekday psalm redistribution, since the source's own per-weekday antiphon pairings don't survive that reslice intact.
This commit is contained in:
@@ -2201,6 +2201,48 @@ saint-name). `tests/hours/compline.test.ts` and `tests/ui/shell.test.ts` updated
|
|||||||
test` (383 tests) and `tsc --noEmit` both pass; manually verified in the browser (English,
|
test` (383 tests) and `tsc --noEmit` both pass; manually verified in the browser (English,
|
||||||
Latin, and both-language modes) against an Advent date with an active octave.
|
Latin, and both-language modes) against an Advent date with an active octave.
|
||||||
|
|
||||||
|
### Ferial weekday Matins nocturn antiphons + versicle — done (2026-08-21)
|
||||||
|
|
||||||
|
Real gap found (user flagged it directly): the plain ferial weekday Matins nocturn
|
||||||
|
(`hours/matins.ts`'s single-nocturn branch, no Sunday/Duplex+ override) rendered bare psalms
|
||||||
|
with no antiphon at all — not a documented design choice like `ferialPsalmodyThreeNocturns`'s
|
||||||
|
antiphon-less fallback, just an oversight from before antiphons existed anywhere in this hour.
|
||||||
|
|
||||||
|
Source: Divinum Officium's Monastic-labeled Matins psalter, `Latin/Psalterium/Psalmi/Psalmi
|
||||||
|
matutinum.txt`'s `[Daym1_]`-`[Daym6_]` sections (the Latin file's own unsuffixed `[Daym1]` etc.
|
||||||
|
just redirects to these via `@:` references), English parallel at the *unsuffixed*
|
||||||
|
`[Daym1]`-`[Daym6]` keys in the `English/` tree (no indirection there). Real wrinkle:
|
||||||
|
`psalter-distribution.yml`'s own `matins:` table isn't the historical per-weekday psalm set —
|
||||||
|
its own header documents a deliberate "editorial redistribution" (54 non-Sunday psalms
|
||||||
|
reclassified must-stay/could-move by length, then resliced into 6 fresh groups of 9 in
|
||||||
|
continuous psalm order), which breaks several of the source's own antiphon-sharing pairs across
|
||||||
|
weekday boundaries (e.g. Ps 43/44, originally Monday's own closing pair, now open Tuesday
|
||||||
|
instead). Resolved by keeping vu's existing 9-psalm weekday lists untouched and regrouping:
|
||||||
|
every psalm keeps its own original antiphon text, consecutive same-antiphon psalms stay grouped
|
||||||
|
whenever their pair survived the reslice intact, and a psalm whose original partner moved
|
||||||
|
elsewhere now stands alone under its own antiphon. Each weekday's single versicle is taken from
|
||||||
|
whichever source day contributes the majority of that weekday's 9 psalms, placed at the same
|
||||||
|
relative position it held there (verified group-by-group against both parallel source files —
|
||||||
|
see each weekday's own comment in the new data file for the exact reasoning).
|
||||||
|
|
||||||
|
New `data/hours/matins-ferial-antiphons.yml` (6 weekdays, 5-6 antiphon groups each, exactly one
|
||||||
|
group per weekday carrying a versicle) + `hours/matins.ts`'s new `ferialAntiphonedNocturn`,
|
||||||
|
replacing the old bare `ferialPsalmody` call in the plain-ferial branch (`ferialPsalmody` itself
|
||||||
|
stays, still used by `ferialPsalmodyThreeNocturns`'s intentionally antiphon-less fallback).
|
||||||
|
Versicle rendered as one combined V./R. block (`"V. ...\nR. ..."`), not just the V. line —
|
||||||
|
`sundayPsalmNocturn`'s own versicle handling drops the R. line entirely (only pushes
|
||||||
|
`group.versicle.v`), which looks like a pre-existing real bug there, not replicated here on
|
||||||
|
purpose; not yet fixed, since it's Sunday's own code, out of scope for this pass.
|
||||||
|
|
||||||
|
`tests/hours/matins.test.ts` gained a new assertion (on the existing `FERIAL_DATE` proof,
|
||||||
|
2026-12-01, Tuesday of Advent I) confirming every nocturn psalm gets its own antiphon,
|
||||||
|
correctly grouped, and exactly one versicle appears at the right point. Also fixed 3 stale
|
||||||
|
Triduum tests that asserted "no versicle part anywhere in the whole ordo" — true only because
|
||||||
|
ferial nocturns had no versicles at all before this change; now checks specifically that the
|
||||||
|
dropped *opening* versicle (and Ps 3, which it precedes) is gone, via `ordo.parts[0]` being the
|
||||||
|
nocturn's own first psalm, rather than asserting zero versicles exist. `npm test` (420 tests)
|
||||||
|
and `tsc --noEmit` both pass.
|
||||||
|
|
||||||
## Known, deliberate simplifications (not bugs — working as designed)
|
## Known, deliberate simplifications (not bugs — working as designed)
|
||||||
|
|
||||||
- `getDayCollects`: each collect in a multi-collect day renders as its own
|
- `getDayCollects`: each collect in a multi-collect day renders as its own
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# Antiphons + versicles for the plain ferial weekday Matins nocturn (no
|
||||||
|
# Sunday, no Duplex+ override) — previously rendered with bare psalms and
|
||||||
|
# no antiphon at all (a genuine oversight, not a documented design choice
|
||||||
|
# like `ferialPsalmodyThreeNocturns`'s antiphon-less fallback).
|
||||||
|
#
|
||||||
|
# `psalter-distribution.yml`'s own `matins:` table is NOT the historical
|
||||||
|
# per-weekday psalm set — its own header documents a deliberate "editorial
|
||||||
|
# redistribution": the 54 non-Sunday psalms were reclassified must-stay/
|
||||||
|
# could-move by length, then resliced into 6 fresh groups of 9 in
|
||||||
|
# continuous psalm order, breaking several original antiphon-sharing pairs
|
||||||
|
# across weekday boundaries in the process (e.g. Ps 43/44 — originally
|
||||||
|
# Monday's own closing pair under "Eructávit" — now open Tuesday instead).
|
||||||
|
#
|
||||||
|
# So this file's own groups do NOT mirror the source's per-weekday
|
||||||
|
# grouping 1:1 — they're regrouped to fit vu's already-fixed 9-psalm list:
|
||||||
|
# every psalm here keeps its own original antiphon text (sourced from
|
||||||
|
# Divinum Officium's Monastic-labeled block, `Latin/Psalterium/Psalmi/
|
||||||
|
# Psalmi matutinum.txt`'s `[Daym1_]`-`[Daym6_]` sections, English parallel
|
||||||
|
# at `[Daym1]`-`[Daym6]` in the `English/` tree — the Latin file only
|
||||||
|
# carries the full per-weekday text under the `_`-suffixed key, redirecting
|
||||||
|
# its own unsuffixed `[Daym1]` etc. to it via `@:` references; the English
|
||||||
|
# file has no such indirection), consecutive same-antiphon psalms are kept
|
||||||
|
# grouped (this still happens whenever a pair survived the reslice
|
||||||
|
# intact), and any psalm whose original partner moved to a different
|
||||||
|
# weekday (or to a minor-hour slot, per `psalter-distribution.yml`'s own
|
||||||
|
# "could-move" psalms) now stands alone under its own antiphon instead.
|
||||||
|
#
|
||||||
|
# Versicle placement: each weekday draws its 9 psalms from 1-2 different
|
||||||
|
# original source days (see each weekday's own comment below for exactly
|
||||||
|
# which), but only one versicle fits per plain single-nocturn weekday —
|
||||||
|
# taken from whichever source day contributes the majority of that
|
||||||
|
# weekday's psalms, placed at the same relative position it held in that
|
||||||
|
# source day's own sequence (immediately after whichever group of this
|
||||||
|
# weekday's psalms was originally its next-door neighbor, once the
|
||||||
|
# psalms that moved out from between them are accounted for).
|
||||||
|
#
|
||||||
|
# Not live-verified against a running query (Monastic ferial ordinary time
|
||||||
|
# isn't independently re-derivable that way once psalter-distribution.yml's
|
||||||
|
# own redistribution has been applied) — sourced directly from the two
|
||||||
|
# parallel psalter files instead, cross-checked psalm-by-psalm against
|
||||||
|
# every entry in psalter-distribution.yml's own `matins:` table above.
|
||||||
|
|
||||||
|
monday:
|
||||||
|
# All 9 psalms are Daym1_'s own (Monastic Matins Feria II) — no reslicing
|
||||||
|
# across weekday boundaries here, so both antiphon grouping and the
|
||||||
|
# versicle sit exactly where Daym1_ itself has them.
|
||||||
|
groups:
|
||||||
|
- psalms: [{ number: 32 }, { number: 33 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Rectos decet * collaudátio."
|
||||||
|
en: "Praise is comely * for the upright."
|
||||||
|
- psalms: [{ number: 34 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Expúgna * impugnántes me."
|
||||||
|
en: "Fight against them * that fight against me."
|
||||||
|
- psalms: [{ number: 36, verses: "1-26" }, { number: 36, verses: "27-40" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Revéla * Dómino viam meam."
|
||||||
|
en: "Commit my way * to the Lord."
|
||||||
|
versicle:
|
||||||
|
v: { la: "Dómine, in cælo misericórdia tua.", en: "O Lord, thy mercy is in heaven," }
|
||||||
|
r: { la: "Et véritas tua usque ad nubes.", en: "And thy truth reacheth even to the clouds." }
|
||||||
|
- psalms: [{ number: 37 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Ne in ira tua * árguas me Dómine."
|
||||||
|
en: "Do not chastise me * in thy wrath."
|
||||||
|
- psalms: [{ number: 38 }, { number: 39 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Ut non delínquam * in lingua mea."
|
||||||
|
en: "That I sin not * with my tongue."
|
||||||
|
- psalms: [{ number: 40 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Sana Dómine * ánimam meam, quia peccávi tibi."
|
||||||
|
en: "Lord, heal my soul; * for I have sinned against thee."
|
||||||
|
|
||||||
|
tuesday:
|
||||||
|
# 43-44 are Monday's own tail (Daym1_); 47-58 are Daym2_'s own opening
|
||||||
|
# (6 of 9 psalms — the majority, hence Daym2_'s own versicle here); 59 is
|
||||||
|
# Wednesday's own opening (Daym3_). The versicle sits right after 49,
|
||||||
|
# exactly where Daym2_ itself has it (between the "49,51" and "52,53"
|
||||||
|
# groups — 51/52/53 all moved elsewhere in the reslice, but 49 and 54
|
||||||
|
# remain each other's nearest neighbor once they're gone).
|
||||||
|
groups:
|
||||||
|
- psalms: [{ number: 43 }, { number: 44 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Eructávit * cor meum verbum bonum."
|
||||||
|
en: "Mine heart * is overflowing with a good matter."
|
||||||
|
- psalms: [{ number: 47 }, { number: 48 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Magnus Dóminus * et laudábilis nimis."
|
||||||
|
en: "Great is the Lord, * and greatly to be praised."
|
||||||
|
- psalms: [{ number: 49 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Deus deórum * Dóminus locútus est."
|
||||||
|
en: "The God of gods * the Lord hath spoken."
|
||||||
|
versicle:
|
||||||
|
v: { la: "Ímmola Deo sacrifícium laudis.", en: "Offer to God the sacrifice of praise." }
|
||||||
|
r: { la: "Et redde Altíssimo vota tua.", en: "And pay thy vows to the most High." }
|
||||||
|
- psalms: [{ number: 54 }, { number: 55 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Inténde in me * et exáudi me."
|
||||||
|
en: "Be attentive to me * and hear me."
|
||||||
|
- psalms: [{ number: 58 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Justa judicáte * fílii hóminum."
|
||||||
|
en: "Judge right things, * ye sons of men."
|
||||||
|
- psalms: [{ number: 59 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Da nobis Dómine * auxílium de tribulatióne."
|
||||||
|
en: "Give us * help from trouble, O Lord."
|
||||||
|
|
||||||
|
wednesday:
|
||||||
|
# 65-72 are Daym3_'s own (7 of 9 — the majority); 73 is Thursday's own
|
||||||
|
# opening (Daym4). The versicle sits between the "67ab" and "68ab"
|
||||||
|
# groups, exactly where Daym3_ itself has it — nothing moved out from
|
||||||
|
# between those two groups in the reslice, so the position is unchanged.
|
||||||
|
groups:
|
||||||
|
- psalms: [{ number: 65 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Benedícite, * gentes, Deum nostrum."
|
||||||
|
en: "O bless our God * ye people."
|
||||||
|
- psalms: [{ number: 67, verses: "2-19" }, { number: 67, verses: "20-36" }]
|
||||||
|
antiphon:
|
||||||
|
la: "In ecclésiis, * benedícite Dóminum."
|
||||||
|
en: "In the churches * bless ye the Lord."
|
||||||
|
versicle:
|
||||||
|
v: { la: "Benedícite, gentes, Deum nostrum.", en: "O bless our God, ye people." }
|
||||||
|
r: { la: "Et audítam fácite vocem laudis ejus.", en: "And make the voice of his praise to be heard." }
|
||||||
|
- psalms: [{ number: 68, verses: "2-16" }, { number: 68, verses: "17-37" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Quǽrite Dóminum * et vivet ánima vestra."
|
||||||
|
en: "Seek ye the Lord * and your soul shall live."
|
||||||
|
- psalms: [{ number: 70 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Dómine * Deus in adjutórium meum inténde."
|
||||||
|
en: "Make haste * O Lord God, to deliver me."
|
||||||
|
- psalms: [{ number: 71 }, { number: 72 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Quam bonus * Israël Deus."
|
||||||
|
en: "How good is God * to Israel."
|
||||||
|
- psalms: [{ number: 73 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Liberásti virgam * hereditátis tuæ."
|
||||||
|
en: "Thou hast redeemed the rod * of Thine inheritance."
|
||||||
|
|
||||||
|
thursday:
|
||||||
|
# All 9 psalms are Daym4's own — no reslicing across weekday boundaries
|
||||||
|
# here either, so grouping and versicle position are both unchanged from
|
||||||
|
# the source (the versicle sits between the "78" and "79,80" groups,
|
||||||
|
# exactly as Daym4 itself has it).
|
||||||
|
groups:
|
||||||
|
- psalms: [{ number: 76 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Tu es Deus * qui facis mirabília."
|
||||||
|
en: "Thou art the God * That doest wonders."
|
||||||
|
- psalms: [{ number: 77, verses: "1-35" }, { number: 77, verses: "36-72" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Inclináte * aurem vestram in verba oris mei."
|
||||||
|
en: "Incline your ears * to the words of My mouth."
|
||||||
|
- psalms: [{ number: 78 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Propítius esto * peccátis nostris, Dómine."
|
||||||
|
en: "Forgive us our sins * O Lord."
|
||||||
|
versicle:
|
||||||
|
v: { la: "Deus, in sancto via tua.", en: "Thy way, O God, is in the holy place." }
|
||||||
|
r: { la: "Quis Deus magnus sicut Deus noster.", en: "Who is the great God like our God?" }
|
||||||
|
- psalms: [{ number: 79 }, { number: 80 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Exsultáte Deo * adjutóri nostro."
|
||||||
|
en: "Sing aloud * unto God our strength."
|
||||||
|
- psalms: [{ number: 82 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Tu solus * altíssimus super omnem terram."
|
||||||
|
en: "Thou alone * art the Most High over all the earth."
|
||||||
|
- psalms: [{ number: 83 }, { number: 84 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Benedixísti * Dómine terram tuam."
|
||||||
|
en: "Thou hast blessed * thy land, O Lord."
|
||||||
|
|
||||||
|
friday:
|
||||||
|
# 85-95 are Daym5_'s own (5 of 9 — the majority: 85, 88ab, 93, 95 — note
|
||||||
|
# 93's original antiphon covered a 92/93 pair, 92 itself having moved
|
||||||
|
# elsewhere, so it's kept here on 93 alone; likewise 95's antiphon
|
||||||
|
# originally also covered 96, which the redistribution's own documented
|
||||||
|
# hand-adjustment moved to Saturday to keep both halves of Ps 103
|
||||||
|
# together there); 101-103 are Daym6_'s own opening (4 of 9). The
|
||||||
|
# versicle sits between the "93" and "95" groups, exactly where Daym5_
|
||||||
|
# itself has it (originally between "92/93" and "95/96" — 96 leaving for
|
||||||
|
# Saturday doesn't touch this boundary).
|
||||||
|
groups:
|
||||||
|
- psalms: [{ number: 85 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Inclína Dómine, * aurem tuam, et exáudi me."
|
||||||
|
en: "Incline thy ear, O Lord, * and hear me."
|
||||||
|
- psalms: [{ number: 88, verses: "2-19" }, { number: 88, verses: "20-53" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Benedíctus * Dóminus in ætérnum."
|
||||||
|
en: "Blessed be * the Lord for evermore."
|
||||||
|
- psalms: [{ number: 93 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Exaltáre, * qui júdicas terram."
|
||||||
|
en: "Lift up thyself, * thou that judgest the earth."
|
||||||
|
versicle:
|
||||||
|
v: { la: "Intret orátio mea in conspéctu tuo, Dómine.", en: "Let my prayer come before Thee, O Lord." }
|
||||||
|
r: { la: "Inclína aurem tuam ad precem meam.", en: "Incline Thine ear unto my cry." }
|
||||||
|
- psalms: [{ number: 95 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Cantáte Dómino, * et benedícite nómini ejus."
|
||||||
|
en: "Sing ye * to the Lord and bless his name."
|
||||||
|
- psalms: [{ number: 101 }, { number: 102 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Clamor meus * ad te véniat, Deus."
|
||||||
|
en: "O God * let my cry come unto Thee."
|
||||||
|
- psalms: [{ number: 103, verses: "1-24" }, { number: 103, verses: "25-35" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Bénedic, * ánima mea Dómino."
|
||||||
|
en: "Bless the Lord, * O my soul."
|
||||||
|
|
||||||
|
saturday:
|
||||||
|
# Ps 96 is Daym5_'s own tail (moved here by the redistribution's own
|
||||||
|
# documented hand-adjustment, swapped out of Friday to let both halves
|
||||||
|
# of Ps 103 land there instead); 104-108 are Daym6_'s own tail (8 of 9 —
|
||||||
|
# the majority, hence Daym6_'s own versicle here). The versicle sits
|
||||||
|
# between the "104ab" and "105ab" groups, exactly where Daym6_ itself
|
||||||
|
# has it — nothing moved out from between those two groups.
|
||||||
|
groups:
|
||||||
|
- psalms: [{ number: 96 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Cantáte Dómino, * et benedícite nómini ejus."
|
||||||
|
en: "Sing ye * to the Lord and bless his name."
|
||||||
|
- psalms: [{ number: 104, verses: "1-22" }, { number: 104, verses: "23-45" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Lætétur cor * quæréntium Dóminum."
|
||||||
|
en: "Let the heart of them rejoice, * that seek the Lord."
|
||||||
|
versicle:
|
||||||
|
v: { la: "Dómine, exáudi oratiónem meam.", en: "O Lord, hear my prayer." }
|
||||||
|
r: { la: "Et clamor meus ad te véniat.", en: "And let my cry come unto thee." }
|
||||||
|
- psalms: [{ number: 105, verses: "1-31" }, { number: 105, verses: "32-48" }]
|
||||||
|
antiphon:
|
||||||
|
la: "Vísita nos, * Dómine, in salutári tuo."
|
||||||
|
en: "Visit us * with Thy salvation, O Lord."
|
||||||
|
- psalms: [{ number: 106, verses: "1-24" }, { number: 106, verses: "25-43" }]
|
||||||
|
antiphon:
|
||||||
|
la: "De necessitátibus meis * éripe me, Dómine."
|
||||||
|
en: "From my necessities, deliver me, O Lord."
|
||||||
|
- psalms: [{ number: 107 }, { number: 108 }]
|
||||||
|
antiphon:
|
||||||
|
la: "Confitébor * Dómino nimis in ore meo."
|
||||||
|
en: "I will greatly praise * the Lord with my mouth."
|
||||||
+54
-1
@@ -84,6 +84,8 @@ import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-read
|
|||||||
import { getOctaveReading } from '../propers/octave-readings';
|
import { getOctaveReading } from '../propers/octave-readings';
|
||||||
import { getMatinsPsalmodyOverride } from './matins-psalmody-overrides';
|
import { getMatinsPsalmodyOverride } from './matins-psalmody-overrides';
|
||||||
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
||||||
|
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
|
||||||
|
import type { Weekday } from '../calendar/types';
|
||||||
|
|
||||||
type BilingualText = Partial<Record<string, string>>;
|
type BilingualText = Partial<Record<string, string>>;
|
||||||
interface SundayGroup {
|
interface SundayGroup {
|
||||||
@@ -114,6 +116,17 @@ interface MatinsSundayAntiphons {
|
|||||||
}
|
}
|
||||||
const sundayAntiphons = matinsSundayAntiphonsData as unknown as MatinsSundayAntiphons;
|
const sundayAntiphons = matinsSundayAntiphonsData as unknown as MatinsSundayAntiphons;
|
||||||
|
|
||||||
|
interface FerialGroup {
|
||||||
|
psalms: PsalmRef[];
|
||||||
|
antiphon: BilingualText;
|
||||||
|
versicle?: { v: BilingualText; r: BilingualText };
|
||||||
|
}
|
||||||
|
interface FerialNocturn {
|
||||||
|
groups: FerialGroup[];
|
||||||
|
}
|
||||||
|
type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>;
|
||||||
|
const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAntiphons;
|
||||||
|
|
||||||
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
|
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
|
||||||
|
|
||||||
type PsalmPart = Extract<ResolvedPart, { kind: 'psalm' }>;
|
type PsalmPart = Extract<ResolvedPart, { kind: 'psalm' }>;
|
||||||
@@ -221,6 +234,46 @@ function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The plain ferial weekday nocturn's real antiphoned psalmody (see
|
||||||
|
* data/hours/matins-ferial-antiphons.yml's own header for how each
|
||||||
|
* weekday's groups/versicle were reconciled against psalter-
|
||||||
|
* distribution.yml's own "editorial redistribution" of the historical
|
||||||
|
* per-weekday psalm set). Every psalm-group carries its own antiphon
|
||||||
|
* (incipit-or-full opening, same rank rule as Sunday's own nocturns), and
|
||||||
|
* exactly one group per weekday carries a versicle+responsory, rendered
|
||||||
|
* as one combined V./R. block (not just the V. line — see the sibling
|
||||||
|
* `sundayPsalmNocturn`'s own versicle handling, which drops the R. line
|
||||||
|
* entirely; not replicated here on purpose). */
|
||||||
|
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
||||||
|
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
||||||
|
const isDouble = isDoubleOrHigher(resolveOfficeWinner(day));
|
||||||
|
const parts: ResolvedPart[] = [];
|
||||||
|
for (const group of nocturn.groups) {
|
||||||
|
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon));
|
||||||
|
const opening = isDouble ? full : incipit;
|
||||||
|
group.psalms.forEach((ref, i) => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
parts.push({ kind: 'antiphon', text: full });
|
||||||
|
if (group.versicle) {
|
||||||
|
const { v, r } = group.versicle;
|
||||||
|
parts.push({
|
||||||
|
kind: 'versicle',
|
||||||
|
text: verifiedText({
|
||||||
|
la: `V. ${v.la}\nR. ${r.la}`,
|
||||||
|
en: `V. ${v.en}\nR. ${r.en}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
|
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
|
||||||
* entry authored for its winner yet (most of them, until the bulk-content
|
* entry authored for its winner yet (most of them, until the bulk-content
|
||||||
* pass — see hours/matins-psalmody-overrides.ts's own doc comment): the
|
* pass — see hours/matins-psalmody-overrides.ts's own doc comment): the
|
||||||
@@ -401,7 +454,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
}
|
}
|
||||||
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
||||||
} else {
|
} else {
|
||||||
parts.push(...ferialPsalmody(day));
|
parts.push(...ferialAntiphonedNocturn(day));
|
||||||
parts.push(...(nocturn1Readings ?? []));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
parts.push({ kind: 'chapter', text: resolveCommon('matins-capitulum-ferial') });
|
parts.push({ kind: 'chapter', text: resolveCommon('matins-capitulum-ferial') });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,30 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
|
|||||||
expect(ordo.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
expect(ordo.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("gives every nocturn psalm its own antiphon and includes Tuesday's own versicle (data/hours/matins-ferial-antiphons.yml)", () => {
|
||||||
|
// The nocturn's own psalms start at index 4 (after versicle/Ps3/heading/Ps94).
|
||||||
|
const nocturnPsalms = ordo.parts.slice(4).filter((p) => p.kind === 'psalm') as {
|
||||||
|
psalmNumber: number;
|
||||||
|
antiphon?: { text: Record<string, string> };
|
||||||
|
}[];
|
||||||
|
expect(nocturnPsalms.map((p) => p.psalmNumber)).toEqual([43, 44, 47, 48, 49, 54, 55, 58, 59]);
|
||||||
|
// Groups: [43,44] [47,48] [49] [54,55] [58] [59] — antiphon only on
|
||||||
|
// each group's first psalm.
|
||||||
|
const withAntiphon = nocturnPsalms.filter((p) => p.antiphon !== undefined).map((p) => p.psalmNumber);
|
||||||
|
expect(withAntiphon).toEqual([43, 47, 49, 54, 58, 59]);
|
||||||
|
for (const p of nocturnPsalms) {
|
||||||
|
if (p.antiphon) {
|
||||||
|
expect(p.antiphon.text.la).toBeTruthy();
|
||||||
|
expect(p.antiphon.text.en).toBeTruthy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nocturnVersicles = ordo.parts.slice(4).filter((p) => p.kind === 'versicle');
|
||||||
|
expect(nocturnVersicles).toHaveLength(1);
|
||||||
|
const versicleText = (nocturnVersicles[0] as { text: { text: Record<string, string> } }).text.text;
|
||||||
|
expect(versicleText.la).toContain('Ímmola Deo sacrifícium laudis');
|
||||||
|
expect(versicleText.la).toContain('Et redde Altíssimo vota tua');
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
||||||
const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
|
||||||
// Plain 2 bible-plan readings on this particular clean ferial (no
|
// Plain 2 bible-plan readings on this particular clean ferial (no
|
||||||
@@ -314,7 +338,12 @@ describe('resolveOrdo("matins", ...) Sacred Triduum', () => {
|
|||||||
|
|
||||||
it.each([HOLY_THURSDAY, GOOD_FRIDAY, HOLY_SATURDAY])('drops the opening versicle/Ps 3/Invitatory/hymn on %s', (date) => {
|
it.each([HOLY_THURSDAY, GOOD_FRIDAY, HOLY_SATURDAY])('drops the opening versicle/Ps 3/Invitatory/hymn on %s', (date) => {
|
||||||
const ordo = resolveOrdo('matins', date);
|
const ordo = resolveOrdo('matins', date);
|
||||||
expect(ordo.parts.some((p) => p.kind === 'versicle')).toBe(false);
|
// The opening versicle itself is gone -- Ps 3 (which it precedes) is
|
||||||
|
// gone too, and the nocturn's own psalmody starts the ordo straight
|
||||||
|
// off (see the "goes straight into Nocturn 1" test below). A versicle
|
||||||
|
// further in belongs to the ferial nocturn's own antiphoned psalmody
|
||||||
|
// (matins-ferial-antiphons.yml), not the dropped opening block.
|
||||||
|
expect(ordo.parts[0]?.kind).toBe('psalm');
|
||||||
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 3)).toBe(false);
|
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 3)).toBe(false);
|
||||||
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 94)).toBe(false);
|
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 94)).toBe(false);
|
||||||
expect(ordo.parts.some((p) => p.kind === 'section-heading' && p.label === 'Invitatory')).toBe(false);
|
expect(ordo.parts.some((p) => p.kind === 'section-heading' && p.label === 'Invitatory')).toBe(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user