Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec448bc35b | |||
| 67037469fc |
@@ -0,0 +1,27 @@
|
||||
// Small ISO-date ("YYYY-MM-DD") arithmetic helpers shared by easter.ts and
|
||||
// temporal.ts. Parsed/formatted as UTC midnight throughout, same convention
|
||||
// as weekday.ts, so results don't shift with the caller's local timezone.
|
||||
|
||||
export interface CalendarDate {
|
||||
year: number;
|
||||
month: number; // 1-12
|
||||
day: number;
|
||||
}
|
||||
|
||||
export function toIsoDate(date: CalendarDate): string {
|
||||
const mm = String(date.month).padStart(2, '0');
|
||||
const dd = String(date.day).padStart(2, '0');
|
||||
return `${date.year}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
export function addDays(isoDate: string, days: number): string {
|
||||
const d = new Date(`${isoDate}T00:00:00Z`);
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function daysBetween(fromIsoDate: string, toIsoDate: string): number {
|
||||
const from = new Date(`${fromIsoDate}T00:00:00Z`).getTime();
|
||||
const to = new Date(`${toIsoDate}T00:00:00Z`).getTime();
|
||||
return Math.round((to - from) / 86_400_000);
|
||||
}
|
||||
+27
-6
@@ -1,8 +1,29 @@
|
||||
// Computus (the date of Easter, and everything the temporal cycle hangs off
|
||||
// of it — Septuagesima, Ash Wednesday, Ascension, Pentecost, Trinity Sunday).
|
||||
// Not implemented yet: unused until milestone 4 (Lauds/Vespers), which is the
|
||||
// first hour content that actually varies by season. Deliberately left empty
|
||||
// rather than half-built ahead of need — see the "content stores" section of
|
||||
// the plan for why calendar-rule logic is being kept swappable rather than
|
||||
// front-loaded.
|
||||
export {};
|
||||
//
|
||||
// This is the Anonymous Gregorian algorithm (a.k.a. Meeus/Jones/Butcher),
|
||||
// valid for the whole Gregorian era (1583 onward). Hand-rolled rather than
|
||||
// pulling in a dependency: it's ~15 lines of pure integer arithmetic with a
|
||||
// long history of independent verification (see calendar/easter.test.ts),
|
||||
// not something that benefits from a library's abstraction or maintenance
|
||||
// surface for an app with zero runtime dependencies otherwise.
|
||||
import type { CalendarDate } from './date-math';
|
||||
|
||||
export function easterSunday(year: number): CalendarDate {
|
||||
const a = year % 19;
|
||||
const b = Math.floor(year / 100);
|
||||
const c = year % 100;
|
||||
const d = Math.floor(b / 4);
|
||||
const e = b % 4;
|
||||
const f = Math.floor((b + 8) / 25);
|
||||
const g = Math.floor((b - f + 1) / 3);
|
||||
const h = (19 * a + b - d - g + 15) % 30;
|
||||
const i = Math.floor(c / 4);
|
||||
const k = c % 4;
|
||||
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
const n = h + l - 7 * m + 114;
|
||||
const month = Math.floor(n / 31);
|
||||
const day = (n % 31) + 1;
|
||||
return { year, month, day };
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { LiturgicalDay } from './types';
|
||||
import { weekdayOf } from './weekday';
|
||||
import { resolveSeason } from './temporal';
|
||||
|
||||
/**
|
||||
* Resolves everything about a given day *except* hour content — weekday,
|
||||
* season, and any occurring feasts. Season and occurring feasts are stubs
|
||||
* until calendar/easter.ts and calendar/feasts.ts land at milestone 4;
|
||||
* hours that only need weekday (Prime, Compline, Terce, Sext, None) can
|
||||
* already rely on this fully.
|
||||
* season, and any occurring feasts. Season is real (see
|
||||
* calendar/temporal.ts); occurring feasts are still a stub until
|
||||
* calendar/feasts.ts lands (the sanctoral calendar — which saint, if any,
|
||||
* is kept on a given day, and Double-vs-not ranking — is a separate,
|
||||
* larger project from temporal-cycle season resolution).
|
||||
*/
|
||||
export function resolveDay(isoDate: string): LiturgicalDay {
|
||||
return {
|
||||
date: isoDate,
|
||||
weekday: weekdayOf(isoDate),
|
||||
// Placeholder string, not a real resolution — real season/temporal-id
|
||||
// lookup (data/calendar/easter-offsets.yml + fixed-date-calendar.yml)
|
||||
// lands at milestone 4.
|
||||
season: 'trinitytide',
|
||||
season: resolveSeason(isoDate),
|
||||
occurring: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,96 @@
|
||||
// Temporal-cycle occurrence resolution: given a date, which temporal-id
|
||||
// applies (season, proper Sunday/feria), by combining two offset systems —
|
||||
// data/calendar/easter-offsets.yml (Septuagesima through Trinitytide) and
|
||||
// data/calendar/fixed-date-calendar.yml (Christmas, Epiphany). Also where
|
||||
// the Advent-start and Epiphanytide-length wrinkles noted in
|
||||
// fixed-date-calendar.yml get arbitrated. Depends on calendar/easter.ts for
|
||||
// the Easter date itself. Unused until milestone 4.
|
||||
export {};
|
||||
// Temporal-cycle occurrence resolution: given a date, which season applies.
|
||||
// Combines two independent anchor systems that hand off to each other once
|
||||
// a year, right around Epiphanytide -> Septuagesima:
|
||||
// - Christmas-relative (data/calendar/fixed-date-calendar.yml): Advent,
|
||||
// Christmastide, Epiphanytide. Advent's start is "the Sunday nearest
|
||||
// Nov 30", not a plain fixed date, so that rule lives in code here.
|
||||
// - Easter-relative (data/calendar/easter-offsets.yml, via
|
||||
// calendar/easter.ts): Septuagesima through the end of Trinitytide.
|
||||
//
|
||||
// Does NOT resolve occurring feasts (calendar/feasts.ts, still a stub) —
|
||||
// a season is the temporal-cycle backdrop a day sits on; which saint (if
|
||||
// any) is being kept that day, and whether it outranks the season, is a
|
||||
// separate, larger project.
|
||||
import type { Season } from './types';
|
||||
import { easterSunday } from './easter';
|
||||
import { addDays, daysBetween, toIsoDate } from './date-math';
|
||||
import fixedDateData from '../data/calendar/fixed-date-calendar.yml';
|
||||
import easterOffsetsData from '../data/calendar/easter-offsets.yml';
|
||||
|
||||
const fixedDates = fixedDateData as { dates: Record<string, string> };
|
||||
const easterOffsets = easterOffsetsData as {
|
||||
ranges: { season: string; fromOffset: number }[];
|
||||
days?: Record<string, string>;
|
||||
};
|
||||
|
||||
function fixedDateFor(id: string): { month: number; day: number } {
|
||||
const entry = Object.entries(fixedDates.dates).find(([, value]) => value === id);
|
||||
if (!entry) {
|
||||
throw new Error(`fixed-date-calendar.yml has no entry for '${id}'`);
|
||||
}
|
||||
const [monthStr, dayStr] = entry[0].split('-');
|
||||
return { month: Number(monthStr), day: Number(dayStr) };
|
||||
}
|
||||
|
||||
const CHRISTMAS = fixedDateFor('christmas-day');
|
||||
const EPIPHANY = fixedDateFor('epiphany');
|
||||
|
||||
/** The Sunday nearest Nov 30 (St. Andrew's Day) — Advent's real start rule. */
|
||||
function adventStart(year: number): string {
|
||||
const nov30 = `${year}-11-30`;
|
||||
const dow = new Date(`${nov30}T00:00:00Z`).getUTCDay(); // 0 = Sunday
|
||||
const delta = dow <= 3 ? -dow : 7 - dow;
|
||||
return addDays(nov30, delta);
|
||||
}
|
||||
|
||||
function seasonFromEasterOffset(offset: number): Season {
|
||||
const dayOverride = easterOffsets.days?.[String(offset)];
|
||||
if (dayOverride) {
|
||||
return dayOverride;
|
||||
}
|
||||
const sorted = [...easterOffsets.ranges].sort((a, b) => a.fromOffset - b.fromOffset);
|
||||
const first = sorted[0];
|
||||
if (!first) {
|
||||
throw new Error('easter-offsets.yml has no ranges');
|
||||
}
|
||||
let season: Season = first.season;
|
||||
for (const range of sorted) {
|
||||
if (offset >= range.fromOffset) {
|
||||
season = range.season;
|
||||
}
|
||||
}
|
||||
return season;
|
||||
}
|
||||
|
||||
export function resolveSeason(isoDate: string): Season {
|
||||
const [, monthStr, dayStr] = isoDate.split('-');
|
||||
const month = Number(monthStr);
|
||||
const day = Number(dayStr);
|
||||
const year = Number(isoDate.slice(0, 4));
|
||||
|
||||
// Advent: the Sunday nearest Nov 30 through Dec 24.
|
||||
const advent = adventStart(year);
|
||||
const isChristmasDayOrLater = month === CHRISTMAS.month && day >= CHRISTMAS.day;
|
||||
if (isoDate >= advent && !isChristmasDayOrLater) {
|
||||
return 'advent';
|
||||
}
|
||||
|
||||
// Christmastide: Christmas Day through Jan 5, wrapping across the civil
|
||||
// year boundary (so checked as two separate month/day windows, not one
|
||||
// date range).
|
||||
if (isChristmasDayOrLater || (month === 1 && day < EPIPHANY.day)) {
|
||||
return 'christmastide';
|
||||
}
|
||||
|
||||
// Epiphanytide: Epiphany until Septuagesima cuts in. Septuagesima's
|
||||
// earliest possible date (Jan 18, when Easter falls on its earliest
|
||||
// possible date, Mar 22) is always after Epiphany, so no year-rollover
|
||||
// Easter lookup is ever needed to make this comparison.
|
||||
const easterIso = toIsoDate(easterSunday(year));
|
||||
const septuagesimaStart = addDays(easterIso, -63);
|
||||
if (isoDate < septuagesimaStart) {
|
||||
return 'epiphanytide';
|
||||
}
|
||||
|
||||
return seasonFromEasterOffset(daysBetween(easterIso, isoDate));
|
||||
}
|
||||
|
||||
+21
-5
@@ -14,6 +14,26 @@ export type Weekday =
|
||||
// actually resolves the "opinionated but configurable" tension: the opinion
|
||||
// (trinitytide) lives in editable data, not in a TS enum you'd recompile to
|
||||
// change.
|
||||
//
|
||||
// A real limitation this doesn't fully solve: a single `season` string can
|
||||
// only hold one mutually-exclusive value per day, but several independent
|
||||
// liturgical windows overlap without sharing boundaries. For example, on a
|
||||
// real day between Candlemas (Feb 2) and Ash Wednesday, Compline's Marian
|
||||
// antiphon should already be Ave Regina Caelorum, but Lent's Alleluia
|
||||
// suppression and hymn swap shouldn't have started yet — two things that
|
||||
// are both true at once, which one `season` value can't represent.
|
||||
//
|
||||
// Real season resolution now exists (calendar/temporal.ts, calendar/easter.ts),
|
||||
// and the Marian-antiphon case above is handled — but not by adding a
|
||||
// `season` value for it. hours/marian-antiphon.ts checks the real date
|
||||
// directly instead of going through `season` at all for that one window.
|
||||
// That's a fine, scoped fix for one known overlap; it isn't a general
|
||||
// solution. If another mechanism turns up with the same shape (a window
|
||||
// that doesn't nest inside one `season` bucket), reach for the same
|
||||
// pattern — a direct date check bypassing `season` — rather than trying to
|
||||
// force `season` to hold two truths at once. Only worth generalizing into
|
||||
// several independent named windows/flags on `LiturgicalDay` itself if a
|
||||
// third case shows up and the duplication starts to hurt.
|
||||
export type Season = string;
|
||||
|
||||
// Open-ended on purpose — the actual ranking scheme (double/semidouble/simple,
|
||||
@@ -35,11 +55,7 @@ export interface LiturgicalDay {
|
||||
/** ISO date, e.g. "2026-08-09" */
|
||||
date: string;
|
||||
weekday: Weekday;
|
||||
/**
|
||||
* Real season resolution depends on Easter's date (see calendar/easter.ts,
|
||||
* not implemented until milestone 4). Until then this is a placeholder and
|
||||
* must not be trusted by any hour's logic.
|
||||
*/
|
||||
/** Real temporal-cycle season, computed via calendar/temporal.ts. */
|
||||
season: Season;
|
||||
/** Always [] until milestone 4 wires up calendar/feasts.ts. */
|
||||
occurring: OccurringFeast[];
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
# Offsets in days from Easter Sunday (0). Negative = before Easter, positive
|
||||
# = after. Covers the whole Easter-anchored span: Septuagesima through the
|
||||
# last Sunday of Trinitytide. A temporal-id's *name* (and hence whether it
|
||||
# reads "trinitytide" or "time after pentecost") lives here as data, not as
|
||||
# a compiled TS enum — see calendar/types.ts's Season type.
|
||||
# last Sunday of Trinitytide. A season's *name* (and hence whether it reads
|
||||
# "trinitytide" or "time after pentecost") lives here as data, not as a
|
||||
# compiled TS enum — see calendar/types.ts's Season type.
|
||||
#
|
||||
# PLACEHOLDER: only a handful of anchor points, to prove the shape. The full
|
||||
# set of Sundays/ferias between anchors is content-authoring work for
|
||||
# milestone 4.
|
||||
offsets:
|
||||
-63: septuagesima-sunday
|
||||
-56: sexagesima-sunday
|
||||
-49: quinquagesima-sunday
|
||||
-46: ash-wednesday
|
||||
0: easter-sunday
|
||||
39: ascension-thursday
|
||||
49: pentecost-sunday
|
||||
56: trinity-sunday
|
||||
# `ranges` gives the season that starts at each offset and runs until the
|
||||
# next range's offset (or, for the last entry, until Advent cuts it off —
|
||||
# see calendar/temporal.ts). `days` overrides a single specific offset with
|
||||
# its own one-day season, for movable feasts that aren't a whole season
|
||||
# but that other by-season tables (prime-chapter-responsory-by-season.yml,
|
||||
# hymn-doxology-by-season.yml) already reference by name.
|
||||
ranges:
|
||||
- season: septuagesima
|
||||
fromOffset: -63
|
||||
- season: lent
|
||||
fromOffset: -46
|
||||
- season: passiontide
|
||||
fromOffset: -14
|
||||
- season: eastertide
|
||||
fromOffset: 0
|
||||
- season: ascensiontide
|
||||
fromOffset: 39
|
||||
- season: pentecost
|
||||
fromOffset: 49
|
||||
- season: trinitytide
|
||||
fromOffset: 56
|
||||
days:
|
||||
60: corpus-christi # Thursday after Trinity Sunday
|
||||
68: sacred-heart # Friday after the octave of Corpus Christi
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Ordo verified against Divinum Officium (Monastic Tridentinum 1617 *and*
|
||||
# Tridentine 1906), 2026-08-25 runs — see src/hours/compline.ts for how
|
||||
# each part resolves.
|
||||
#
|
||||
# Base is Monastic 1617, per explicit choice, with several deliberate
|
||||
# additions from Tridentine 1906 (Monastic Compline has none of these):
|
||||
# - The "In manus tuas" short responsory, right after the capitulum.
|
||||
# - The Nunc Dimittis, framed by its "Salva nos" antiphon, right after
|
||||
# the responsory's versicle.
|
||||
# - The fuller Tridentine Preces, in place of Monastic's own shorter form
|
||||
# (which drops the "Benedíctus es, Dómine..." triple exchange and the
|
||||
# "Miserére nostri, Dómine.../Fiat misericórdia..." exchange — both
|
||||
# marked "(sed rubrica ^Monastic omittitur)" in the source). Compline
|
||||
# has no separate ferial/Sunday Preces text the way Prime does — one
|
||||
# form, gated only by omitOnDouble (see calendar/isDoubleOrHigher).
|
||||
# - Lent's distinct hymn replacement ("Christe, qui lux es et dies") —
|
||||
# see hours/compline.ts's 'hymn' case for a caveat on how confident to
|
||||
# be that this actually triggers automatically in real practice.
|
||||
#
|
||||
# Unlike Prime, Compline's psalms (4, 90, 133) are fixed every day — no
|
||||
# weekday rotation — and have no antiphon of their own at all in ferial use.
|
||||
# The capitulum and Preces are likewise not split Sunday-vs-ferial.
|
||||
#
|
||||
# The closing Marian antiphon is genuinely seasonal in real practice (Alma
|
||||
# Redemptoris Mater / Ave Regina Caelorum / Regina Caeli / Salve Regina) —
|
||||
# see hours/marian-antiphon.ts.
|
||||
id: compline
|
||||
parts:
|
||||
- kind: preces
|
||||
textRef: { source: common, id: compline-incipit }
|
||||
- kind: lesson
|
||||
textRef: { source: common, id: compline-lectio-brevis }
|
||||
label: Short Reading
|
||||
- kind: preces
|
||||
textRef: { source: common, id: lesson-close }
|
||||
- kind: preces
|
||||
textRef: { source: common, id: compline-confiteor }
|
||||
- kind: opening-versicle
|
||||
- kind: psalm
|
||||
psalmNumber: 4
|
||||
- kind: psalm
|
||||
psalmNumber: 90
|
||||
- kind: psalm
|
||||
psalmNumber: 133
|
||||
- kind: hymn
|
||||
textRef: { source: common, id: compline-hymn }
|
||||
- kind: chapter
|
||||
textRef: { source: common, id: compline-capitulum }
|
||||
- kind: responsory
|
||||
textRef: { source: common, id: compline-responsory }
|
||||
- kind: versicle
|
||||
textRef: { source: common, id: compline-versicle }
|
||||
- kind: nunc-dimittis
|
||||
- kind: preces
|
||||
textRef: { source: common, id: compline-preces }
|
||||
omitOnDouble: true
|
||||
- kind: prayer
|
||||
textRef: { source: common, id: compline-collect }
|
||||
- kind: preces
|
||||
textRef: { source: common, id: compline-conclusio }
|
||||
label: Conclusion
|
||||
- kind: marian-antiphon
|
||||
@@ -1,9 +1,11 @@
|
||||
# Which common proper supplies the hymn's final doxology stanza, by season.
|
||||
# Advent has no special doxology in the source data — it falls through to
|
||||
# perAnnum, same as any season not listed here. Same PLACEHOLDER caveat as
|
||||
# Shared across every hymn (Divinum Officium's own Doxologies.txt table is
|
||||
# hymn-agnostic) — each hymn's own per-annum default is supplied by the
|
||||
# caller instead (see hours/hymn-doxology.ts). Advent has no special
|
||||
# doxology in the source data — it falls through to the caller's per-annum
|
||||
# default, same as any season not listed here. Same PLACEHOLDER caveat as
|
||||
# prime-chapter-responsory-by-season.yml: real season resolution doesn't
|
||||
# exist until milestone 4, so this can't be exercised by real data yet.
|
||||
perAnnum: hymn-doxology-per-annum
|
||||
bySeason:
|
||||
christmastide: hymn-doxology-nat
|
||||
epiphanytide: hymn-doxology-epi
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Which common proper supplies Compline's closing Marian antiphon, by
|
||||
# season. Only 4 real forms exist regardless of how finely other mechanisms
|
||||
# (chapter responsory, hymn doxology) slice the year — Ascensiontide and
|
||||
# Pentecost don't get their own Marian antiphon, they're still "Paschalis"
|
||||
# (Regina Caeli); Epiphanytide is still "Nativiti" (Alma Redemptoris Mater).
|
||||
#
|
||||
# Ave Regina Caelorum's real window (Candlemas, Feb 2, through Wednesday of
|
||||
# Holy Week) doesn't line up with any single `season` value — it starts
|
||||
# mid-Epiphanytide and ends mid-Lent or mid-Passiontide depending on the
|
||||
# year, cutting across three of the mutually-exclusive season buckets used
|
||||
# everywhere else in this app. So it's NOT a key in this table at all;
|
||||
# hours/marian-antiphon.ts checks that window directly against the real
|
||||
# date before ever consulting bySeason below. See calendar/types.ts's
|
||||
# Season doc comment for the general overlapping-windows problem this is
|
||||
# one instance of.
|
||||
perAnnum: marian-antiphon-salve-regina
|
||||
bySeason:
|
||||
advent: marian-antiphon-alma-redemptoris-advent
|
||||
christmastide: marian-antiphon-alma-redemptoris-nativity
|
||||
epiphanytide: marian-antiphon-alma-redemptoris-nativity
|
||||
eastertide: marian-antiphon-regina-caeli
|
||||
ascensiontide: marian-antiphon-regina-caeli
|
||||
pentecost: marian-antiphon-regina-caeli
|
||||
@@ -3,8 +3,8 @@
|
||||
# drop the Alleluia — same PLACEHOLDER caveat as the other by-season
|
||||
# tables: real season resolution doesn't exist until milestone 4, so this
|
||||
# always falls through to perAnnum today.
|
||||
perAnnum: prime-opening-alleluia
|
||||
perAnnum: opening-alleluia
|
||||
bySeason:
|
||||
septuagesima: prime-opening-laus-tibi
|
||||
lent: prime-opening-laus-tibi
|
||||
passiontide: prime-opening-laus-tibi
|
||||
septuagesima: opening-laus-tibi
|
||||
lent: opening-laus-tibi
|
||||
passiontide: opening-laus-tibi
|
||||
@@ -47,6 +47,7 @@ parts:
|
||||
textRef: { source: common, id: prime-versicle }
|
||||
- kind: by-day-kind
|
||||
resolvedKind: preces
|
||||
omitOnDouble: true
|
||||
sundayOrFeastRef: { source: common, id: prime-preces-dominicales }
|
||||
ferialRef: { source: common, id: prime-preces-feriales }
|
||||
- kind: prayer
|
||||
@@ -59,7 +60,7 @@ parts:
|
||||
textRef: { source: common, id: prime-lectio-brevis-per-annum }
|
||||
label: Short Reading
|
||||
- kind: preces
|
||||
textRef: { source: common, id: prime-lesson-close }
|
||||
textRef: { source: common, id: lesson-close }
|
||||
- kind: preces
|
||||
textRef: { source: common, id: prime-conclusio }
|
||||
label: Conclusion
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Fixed year-round, and (unlike Prime) not split Sunday/feast vs.
|
||||
# ferial — Compline's capitulum has only this one form.
|
||||
id: compline-capitulum
|
||||
text:
|
||||
la: "Tu autem in nobis es, Dómine, et nomen sanctum tuum invocátum est super nos: ne derelínquas nos, Dómine, Deus noster."
|
||||
en: "But thou, O Lord, art among us, and thy name is called upon by us: forsake us not, O Lord our God."
|
||||
citation:
|
||||
la: "Jer. 14:9"
|
||||
en: "Jer. 14:9"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,14 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25 run.
|
||||
id: compline-collect
|
||||
text:
|
||||
la: |
|
||||
Orémus.
|
||||
Vísita, quǽsumus, Dómine, habitatiónem istam, et omnes insídias inimíci ab ea longe repélle: Ángeli tui sancti hábitent in ea, qui nos in pace custódiant; et benedíctio tua sit super nos semper. Per Dóminum nostrum Jesum Christum, Fílium tuum: qui tecum vivit et regnat in unitáte Spíritus Sancti, Deus, per ómnia sǽcula sæculórum.
|
||||
R. Amen.
|
||||
en: |
|
||||
Let us pray.
|
||||
Visit, we beseech thee, O Lord, this dwelling, and drive far from it the snares of the enemy; let thy holy angels dwell herein to preserve us in peace, and let thy blessing be always upon us. Through Jesus Christ, thy Son our Lord, Who liveth and reigneth with thee, in the unity of the Holy Ghost, God, world without end.
|
||||
R. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,22 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Distinct wording from Prime's own Conclusio — different versicle
|
||||
# pairing, different (Trinity-formula) blessing.
|
||||
id: compline-conclusio
|
||||
text:
|
||||
la: |
|
||||
V. Dómine, exáudi oratiónem meam.
|
||||
R. Et clamor meus ad te véniat.
|
||||
V. Benedicámus Dómino.
|
||||
R. Deo grátias.
|
||||
Benedictio. Benedícat et custódiat nos omnípotens et miséricors Dóminus, ✠ Pater, et Fílius, et Spíritus Sanctus.
|
||||
R. Amen.
|
||||
en: |
|
||||
V. O Lord, hear my prayer.
|
||||
R. And let my cry come unto thee.
|
||||
V. Let us bless the Lord.
|
||||
R. Thanks be to God.
|
||||
Benediction. The almighty and merciful Lord, ✠ the Father, the Son and the Holy Spirit, bless us and keep us.
|
||||
R. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,37 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Compline's Confiteor cluster comes *before* the psalms (unlike
|
||||
# Prime's, which comes after) — said right after the Lectio Brevis, then
|
||||
# straight into "Deus in adiutorium" (see hours/opening-versicle.ts).
|
||||
id: compline-confiteor
|
||||
text:
|
||||
la: |
|
||||
V. Adjutórium nostrum ✠ in nómine Dómini.
|
||||
R. Qui fecit cælum et terram.
|
||||
|
||||
Pater noster, qui es in cælis, sanctificétur nomen tuum: advéniat regnum tuum: fiat volúntas tua, sicut in cælo et in terra. Panem nostrum quotidiánum da nobis hódie: et dimítte nobis débita nostra, sicut et nos dimíttimus debitóribus nostris: et ne nos indúcas in tentatiónem: sed líbera nos a malo. Amen.
|
||||
|
||||
Confíteor Deo omnipoténti, beátæ Maríæ semper Vírgini, beáto Michaéli Archángelo, beáto Joánni Baptístæ, sanctis Apóstolis Petro et Paulo, beáto Patri nostro Benedícto, et ómnibus Sanctis, quia peccávi nimis, cogitatióne, verbo et ópere: mea culpa, mea culpa, mea máxima culpa. Ídeo precor beátam Maríam semper Vírginem, beátum Michaélem Archángelum, beátum Joánnem Baptístam, sanctos Apóstolos Petrum et Paulum, beátum Patrem nostrum Benedíctum, et omnes Sanctos, oráre pro me ad Dóminum Deum nostrum.
|
||||
|
||||
Misereátur nostri omnípotens Deus, et dimíssis peccátis nostris, perdúcat nos ad vitam ætérnam. Amen.
|
||||
|
||||
Indulgéntiam, ✠ absolutiónem et remissiónem peccatórum nostrórum tríbuat nobis omnípotens et miséricors Dóminus. Amen.
|
||||
|
||||
V. Convérte nos ✙︎ Deus, salutáris noster.
|
||||
R. Et avérte iram tuam a nobis.
|
||||
en: |
|
||||
V. Our help is ✠ in the name of the Lord,
|
||||
R. Who made heaven and earth.
|
||||
|
||||
Our Father, who art in heaven, Hallowed be thy name. Thy kingdom come. Thy will be done on earth as it is in heaven. Give us this day our daily bread. And forgive us our trespasses, as we forgive those who trespass against us. And lead us not into temptation: But deliver us from evil. Amen.
|
||||
|
||||
I confess to almighty God, to blessed Mary ever Virgin, to blessed Michael the Archangel, to blessed John the Baptist, to the holy Apostles Peter and Paul, our holy Father Benedict, and to all the Saints, that I have sinned exceedingly in thought, word and deed: through my fault, through my fault, through my most grievous fault. Therefore I beseech blessed Mary ever Virgin, blessed Michael the Archangel, blessed John the Baptist, the holy Apostles Peter and Paul, our holy Father Benedict, and all the Saints, to pray for me to the Lord our God.
|
||||
|
||||
May almighty God have mercy on us, forgive us our sins, and bring us to everlasting life. Amen.
|
||||
|
||||
May the almighty ✠ and merciful Lord grant us pardon, absolution and remission of our sins. Amen.
|
||||
|
||||
V. Turn us then, ✙︎ O God, our saviour:
|
||||
R. And let thy anger cease from us.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,12 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Compline hymn's own default final stanza — the seasonal overrides
|
||||
# (hymn-doxology-nat.yml etc.) are shared across every hymn's doxology slot,
|
||||
# since Divinum Officium's own Doxologies.txt table is hymn-agnostic; only
|
||||
# the per-annum default is specific to a given hymn.
|
||||
id: compline-hymn-doxology-per-annum
|
||||
text:
|
||||
la: "Præsta, Pater omnípotens,\nPer Jesum Christum Dóminum,\nQui tecum in perpétuum\nRegnat cum Sancto Spíritu.\nAmen."
|
||||
en: "O Father, that we ask be done,\nThrough Jesus Christ, thine only Son;\nWho, with the Holy Ghost and thee,\nDoth live and reign eternally.\nAmen."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,106 @@
|
||||
# Verified against Divinum Officium ("Minor Special.txt", [Hymnus
|
||||
# Completorium Quad]) — "Christe, qui lux es et dies", a complete
|
||||
# alternate hymn (not just a different doxology) traditionally used at
|
||||
# Compline during Lent. Self-contained: its own final stanza is already
|
||||
# the standard doxology, so this isn't run through hymn-doxology.ts the
|
||||
# way compline-hymn.yml is.
|
||||
#
|
||||
# No verified English translation exists in the local Divinum Officium
|
||||
# dataset for this specific variant. Real 19th-century public-domain
|
||||
# translations do exist (W. J. Copeland, 1848/1867, in Hymns Ancient and
|
||||
# Modern; also Mrs. Rundle Charles 1858, W. Mercer 1864, R. F. Littledale
|
||||
# 1867) but a complete, verbatim, accurately-sourced text of any of them
|
||||
# wasn't findable — rather than reconstruct one from memory and risk
|
||||
# misattributing invented wording to a real historical translator, the
|
||||
# English below is a fresh, direct translation, marked "draft" (not
|
||||
# "verified" like everything else in this project) for exactly that
|
||||
# reason. Swap in a real historical translation instead if you track one
|
||||
# down. The final stanza's translation is the exception — it's identical
|
||||
# Latin to compline-hymn-doxology-per-annum.yml's own stanza, so that
|
||||
# already-verified English is reused here.
|
||||
#
|
||||
# Also worth flagging: I could not get the reference engine to actually
|
||||
# select this hymn for a Lenten date under Tridentine 1906 (it kept
|
||||
# returning the standard "Te lucis ante terminum"), and found no code path
|
||||
# that appends a seasonal suffix for Completorium's hymn at all — so this
|
||||
# may be legacy/unused data in that particular engine rather than
|
||||
# something it auto-selects. Wired in here as a deliberate editorial
|
||||
# choice (the hymn and its Lenten use are independently well-attested),
|
||||
# not a confirmed live behavior.
|
||||
id: compline-hymn-lenten
|
||||
text:
|
||||
la: |
|
||||
Christe, qui lux es et dies,
|
||||
Noctis tenébras détegis,
|
||||
Lucísque lumen créderis,
|
||||
Lumen beátum prǽdicans.
|
||||
|
||||
Precámur sancte Dómine,
|
||||
defénde nos in hac nocte:
|
||||
Sit nobis in te réquies,
|
||||
Quiétam noctem tríbue.
|
||||
|
||||
Ne gravis somnus írruat,
|
||||
Nec hostis nos subrípiat,
|
||||
Nec caro illi conséntiens
|
||||
Nos tibi reos státuat.
|
||||
|
||||
Oculi somnum cápiant,
|
||||
Cor ad te semper vígilet:
|
||||
Déxtera tua prótegat
|
||||
Fámulos, qui te díligunt.
|
||||
|
||||
Defénsor noster áspice,
|
||||
Insidiántes réprime,
|
||||
Gubérna tuos fámulos,
|
||||
Quos Sánguine mercátus es.
|
||||
|
||||
Meménto nostri, Dómine,
|
||||
In gravi isto córpore:
|
||||
Qui es defénsor ánimæ,
|
||||
Adésto nobis, Dómine.
|
||||
|
||||
Præsta Pater omnípotens,
|
||||
Per Jesum Christum Dóminum,
|
||||
Qui tecum in perpétuum
|
||||
Regnat cum Sancto Spíritu.
|
||||
Amen.
|
||||
en: |
|
||||
O Christ, who art the light and day,
|
||||
who dost dispel the darkness of night,
|
||||
and art believed to be the light of light,
|
||||
proclaiming blessed light.
|
||||
|
||||
We beseech thee, holy Lord,
|
||||
defend us this night:
|
||||
let our rest be in thee alone,
|
||||
grant us a quiet night.
|
||||
|
||||
Let not heavy sleep overtake us,
|
||||
nor let the enemy steal upon us;
|
||||
let not the flesh, consenting to him,
|
||||
render us guilty before thee.
|
||||
|
||||
Let our eyes take their sleep,
|
||||
but let our heart keep watch for thee always:
|
||||
let thy right hand protect
|
||||
thy servants who love thee.
|
||||
|
||||
Look upon us, our defender;
|
||||
restrain those who lie in wait;
|
||||
govern thy servants,
|
||||
whom thou hast purchased with thy Blood.
|
||||
|
||||
Remember us, O Lord,
|
||||
in this burdened body:
|
||||
thou who art the defender of the soul,
|
||||
be present with us, O Lord.
|
||||
|
||||
O Father, that we ask be done,
|
||||
Through Jesus Christ, thine only Son;
|
||||
Who, with the Holy Ghost and thee,
|
||||
Doth live and reign eternally.
|
||||
Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: draft
|
||||
@@ -0,0 +1,36 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. "Te lucis ante terminum" — only 2 body stanzas (the doxology, its 3rd
|
||||
# stanza, is stored separately since it varies by season — see
|
||||
# compline-hymn-doxology-per-annum.yml and hours/hymn-doxology.ts, same
|
||||
# mechanism as Prime's hymn).
|
||||
#
|
||||
# Monastic wording ("sólita"/"ad custódiam") differs slightly from the
|
||||
# Roman "pro tua"/"et custódia" — this is the Monastic form.
|
||||
#
|
||||
# Lent has its own distinct 6-stanza hymn ("Christe, qui lux es et dies")
|
||||
# rather than just a different doxology on this one — not modeled yet.
|
||||
id: compline-hymn
|
||||
text:
|
||||
la: |
|
||||
Te lucis ante términum,
|
||||
Rerum Creátor, póscimus,
|
||||
Ut sólita cleméntia
|
||||
Sis præsul ad custódiam.
|
||||
|
||||
Procul recédant sómnia,
|
||||
Et nóctium phantásmata;
|
||||
Hostémque nostrum cómprime,
|
||||
Ne polluántur córpora.
|
||||
en: |
|
||||
Before the ending of the day,
|
||||
Creator of the world, we pray
|
||||
That with thy wonted favor thou
|
||||
Wouldst be our guard and keeper now.
|
||||
|
||||
From all ill dreams defend our eyes,
|
||||
From nightly fears and fantasies;
|
||||
Tread under foot our ghostly foe,
|
||||
That no pollution we may know.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,17 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Compline's own opening blessing — distinct from Prime's Regula
|
||||
# blessing ("Dies et actus nostros...") despite the shared "Jube, domne"
|
||||
# request line.
|
||||
id: compline-incipit
|
||||
text:
|
||||
la: |
|
||||
V. Jube, domne, benedícere.
|
||||
Benedictio. Noctem quiétam et finem perféctum concédat nobis Dóminus omnípotens.
|
||||
R. Amen.
|
||||
en: |
|
||||
V. Grant, Lord, a blessing.
|
||||
Benediction. May almighty God grant us a quiet night and a perfect end.
|
||||
R. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,13 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Fixed year-round in the source data (no seasonal variants the way
|
||||
# Prime's per-annum lectio brevis is flagged as eventually needing).
|
||||
id: compline-lectio-brevis
|
||||
text:
|
||||
la: "Fratres: Sóbrii estóte, et vigiláte: quia adversárius vester diábolus tamquam leo rúgiens círcuit, quærens quem dévoret: cui resístite fortes in fide."
|
||||
en: "Brothers: Be sober and watch: because your adversary the devil, as a roaring lion, goeth about seeking whom he may devour. Whom resist ye, strong in faith."
|
||||
citation:
|
||||
la: "1 Pet. 5:8-9"
|
||||
en: "1 Pet. 5:8-9"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,69 @@
|
||||
# Verified against Divinum Officium (Tridentine 1906), 2026-08-25 run —
|
||||
# used by explicit choice instead of Monastic's own (shorter) Compline
|
||||
# Preces, which drop the "Benedíctus es, Dómine..." triple exchange at the
|
||||
# start and the "Miserére nostri, Dómine.../Fiat misericórdia..." exchange
|
||||
# near the end (both marked "(sed rubrica ^Monastic omittitur)" in the
|
||||
# source). Compline has no separate ferial/Sunday form the way Prime does —
|
||||
# this is the only text; the only variable is whether Preces are said at
|
||||
# all (see calendar/isDoubleOrHigher — dropped on a Double-or-higher feast).
|
||||
id: compline-preces
|
||||
text:
|
||||
la: |
|
||||
Kýrie, eléison. Christe, eléison. Kýrie, eléison.
|
||||
|
||||
Pater noster, qui es in cælis, sanctificétur nomen tuum: advéniat regnum tuum: fiat volúntas tua, sicut in cælo et in terra. Panem nostrum quotidiánum da nobis hódie: et dimítte nobis débita nostra, sicut et nos dimíttimus debitóribus nostris:
|
||||
V. Et ne nos indúcas in tentatiónem:
|
||||
R. Sed líbera nos a malo.
|
||||
|
||||
Credo in Deum, Patrem omnipoténtem, Creatórem cæli et terræ. Et in Jesum Christum, Fílium ejus únicum, Dóminum nostrum: qui concéptus est de Spíritu Sancto, natus ex María Vírgine, passus sub Póntio Piláto, crucifíxus, mórtuus, et sepúltus: descéndit ad ínferos; tértia die resurréxit a mórtuis; ascéndit ad cælos; sedet ad déxteram Dei Patris omnipoténtis: inde ventúrus est judicáre vivos et mórtuos. Credo in Spíritum Sanctum, sanctam Ecclésiam cathólicam, Sanctórum communiónem, remissiónem peccatórum.
|
||||
V. Carnis resurrectiónem.
|
||||
R. Vitam ætérnam. Amen.
|
||||
|
||||
V. Benedíctus es, Dómine, Deus patrum nostrórum.
|
||||
R. Et laudábilis et gloriósus in sǽcula.
|
||||
V. Benedicámus Patrem et Fílium cum Sancto Spíritu.
|
||||
R. Laudémus, et superexaltémus eum in sǽcula.
|
||||
V. Benedíctus es, Dómine, in firmaménto cæli.
|
||||
R. Et laudábilis, et gloriósus, et superexaltátus in sǽcula.
|
||||
V. Benedícat et custódiat nos omnípotens et miséricors Dóminus.
|
||||
R. Amen.
|
||||
|
||||
V. Dignáre, Dómine, nocte ista.
|
||||
R. Sine peccáto nos custodíre.
|
||||
V. Miserére nostri, Dómine.
|
||||
R. Miserére nostri.
|
||||
V. Fiat misericórdia tua, Dómine, super nos.
|
||||
R. Quemádmodum sperávimus in te.
|
||||
V. Dómine, exáudi oratiónem meam.
|
||||
R. Et clamor meus ad te véniat.
|
||||
en: |
|
||||
Lord have mercy upon us, Christ have mercy upon us, Lord have mercy upon us.
|
||||
|
||||
Our Father, who art in heaven, Hallowed be thy name. Thy kingdom come. Thy will be done on earth as it is in heaven. Give us this day our daily bread. And forgive us our trespasses, as we forgive those who trespass against us.
|
||||
V. And lead us not into temptation:
|
||||
R. But deliver us from evil.
|
||||
|
||||
I believe in God, the Father almighty, Creator of heaven and earth. And in Jesus Christ, his only Son, our Lord; who was conceived by the Holy Ghost, born of the Virgin Mary, suffered under Pontius Pilate, was crucified, died and was buried: he descended into hell; the third day he arose again from the dead; he ascended into heaven; sitteth at the right hand of God the Father almighty: from thence he shall come to judge the living and the dead. I believe in the Holy Ghost, the holy catholic Church, the communion of Saints, the forgiveness of sins.
|
||||
V. The resurrection of the body.
|
||||
R. And life everlasting. Amen.
|
||||
|
||||
V. Blessed are you, Lord, God of our fathers.
|
||||
R. Worthy of praise, and glorious for ever.
|
||||
V. Let us bless the Father, the Son and the Holy Spirit.
|
||||
R. Let us praise and exalt him above all for ever.
|
||||
V. Blessed art thou in the firmament of heaven:
|
||||
R. Worthy of praise, and glorious for ever.
|
||||
V. May the almighty and merciful God bless us and keep us.
|
||||
R. Amen.
|
||||
|
||||
V. Vouchsafe, O Lord,
|
||||
R. To keep us this night without sin.
|
||||
V. O Lord, have mercy upon us.
|
||||
R. Have mercy upon us.
|
||||
V. O Lord, let thy mercy lighten upon us
|
||||
R. As our trust is in you.
|
||||
V. O Lord, hear my prayer.
|
||||
R. And let my cry come unto thee.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,24 @@
|
||||
# Verified against Divinum Officium (Tridentine 1906), 2026-08-25 run —
|
||||
# Monastic Compline doesn't have this at all (like Prime's borrowed
|
||||
# responsory); added by explicit choice. Uses the non-Dominican verse
|
||||
# ("Redemísti nos" / "us") — the Order of Preachers rubric substitutes
|
||||
# "Redemísti me" ("me") instead, not used here.
|
||||
id: compline-responsory
|
||||
text:
|
||||
la: |
|
||||
R.br. In manus tuas, Dómine, * Comméndo spíritum meum.
|
||||
R. In manus tuas, Dómine, * Comméndo spíritum meum.
|
||||
V. Redemísti nos, Dómine, Deus veritátis.
|
||||
R. Comméndo spíritum meum.
|
||||
V. Glória Patri, et Fílio, * et Spirítui Sancto.
|
||||
R. In manus tuas, Dómine, * Comméndo spíritum meum.
|
||||
en: |
|
||||
R.br. Into thy hands, O Lord, * do I commend my spirit.
|
||||
R. Into thy hands, O Lord, * do I commend my spirit.
|
||||
V. For thou hast redeemed us, O Lord, God of truth.
|
||||
R. I commend my spirit.
|
||||
V. Glory be to the Father, and to the Son, * and to the Holy Ghost.
|
||||
R. Into thy hands, O Lord, * do I commend my spirit.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,12 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25 run.
|
||||
id: compline-versicle
|
||||
text:
|
||||
la: |
|
||||
V. Custódi nos, Dómine, ut pupíllam óculi.
|
||||
R. Sub umbra alárum tuárum prótege nos.
|
||||
en: |
|
||||
V. Keep us, Lord, as the apple of thine eye.
|
||||
R. Protect us under the shadow of thy wings.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25 run.
|
||||
# Closing dialogue shared by both the Regula reading and the Lectio Brevis
|
||||
# that follows it.
|
||||
id: prime-lesson-close
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. Closing dialogue after any short reading — shared across hours
|
||||
# (Prime's Regula reading and Lectio Brevis, Compline's own Lectio Brevis).
|
||||
id: lesson-close
|
||||
text:
|
||||
la: |
|
||||
V. Tu autem, Dómine, miserére nobis.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Verified against Divinum Officium (Latin/English Mariaant.txt, [Advent]).
|
||||
# Same antiphon text as the Christmastide form (marian-antiphon-alma-
|
||||
# redemptoris-nativity.yml) but a different versicle and collect — Advent
|
||||
# looks forward to the Incarnation, Christmastide back on it having happened.
|
||||
id: marian-antiphon-alma-redemptoris-advent
|
||||
text:
|
||||
la: |
|
||||
Alma Redemptóris Mater, quæ pérvia cæli porta manes, et stella maris, succúrre cadénti, súrgere qui curat, pópulo: tu quæ genuísti, Natúra miránte, tuum sanctum Genitórem, Virgo prius ac postérius, Gabriélis ab ore sumens illud Ave, peccatórum miserére.
|
||||
|
||||
V. Ángelus Dómini nuntiávit Maríæ.
|
||||
R. Et concépit de Spíritu Sancto.
|
||||
|
||||
Orémus.
|
||||
Grátiam tuam, quǽsumus, Dómine, méntibus nostris infúnde: ut, qui, Ángelo nuntiánte, Christi Fílii tui incarnatiónem cognóvimus; per passiónem ejus et crucem, ad resurrectiónis glóriam perducámur. Per eúmdem Christum Dóminum nostrum. Amen.
|
||||
en: |
|
||||
Mother of Christ, hear thou thy people's cry, Star of the deep and Portal of the sky! Mother of Him who thee from nothing made. Sinking we strive and call to thee for aid: O, by what joy which Gabriel brought to thee, Thou Virgin first and last, let us thy mercy see.
|
||||
|
||||
V. The Angel of the Lord announced unto Mary.
|
||||
R. And she conceived of the Holy Spirit.
|
||||
|
||||
Let us pray.
|
||||
Pour forth, we beseech thee, O Lord, thy grace into our hearts: that as we have known the incarnation of thy Son Jesus Christ by the message of an Angel, so too by his Cross and passion may we be brought to the glory of his resurrection. Through the same Christ our Lord. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,24 @@
|
||||
# Verified against Divinum Officium (Latin/English Mariaant.txt, [Nativiti]).
|
||||
# Used from Christmas through Purification (Feb 2), covering Epiphanytide
|
||||
# too — same antiphon text as Advent's form, different versicle/collect.
|
||||
id: marian-antiphon-alma-redemptoris-nativity
|
||||
text:
|
||||
la: |
|
||||
Alma Redemptóris Mater, quæ pérvia cæli porta manes, et stella maris, succúrre cadénti, súrgere qui curat, pópulo: tu quæ genuísti, Natúra miránte, tuum sanctum Genitórem, Virgo prius ac postérius, Gabriélis ab ore sumens illud Ave, peccatórum miserére.
|
||||
|
||||
V. Post partum, Virgo, invioláta permansísti.
|
||||
R. Dei Génetrix, intercéde pro nobis.
|
||||
|
||||
Orémus.
|
||||
Deus, qui salútis ætérnæ, beátæ Maríæ virginitáte fecúnda, humáno géneri prǽmia præstitísti: tríbue, quǽsumus; ut ipsam pro nobis intercédere sentiámus, per quam merúimus auctórem vitæ suscípere, Dóminum nostrum Jesum Christum Fílium tuum. Amen.
|
||||
en: |
|
||||
Mother of Christ, hear thou thy people's cry, Star of the deep and Portal of the sky! Mother of Him who thee from nothing made. Sinking we strive and call to thee for aid: O, by what joy which Gabriel brought to thee, Thou Virgin first and last, let us thy mercy see.
|
||||
|
||||
V. After childbirth thou didst remain a virgin.
|
||||
R. Intercede for us, O Mother of God.
|
||||
|
||||
Let us pray.
|
||||
O God, who, by the fruitful virginity of blessed Mary, hast bestowed upon mankind the reward of eternal salvation: grant, we beseech thee, that we may experience her intercession, through whom we have been made worthy to receive the author of life, Our Lord Jesus Christ thy Son. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,28 @@
|
||||
# Verified against Divinum Officium (Latin/English Mariaant.txt,
|
||||
# [Quadragesimae]). Real boundary is Purification (Feb 2) through Wednesday
|
||||
# of Holy Week — *not* the same start as the Alleluia-suppression boundary
|
||||
# (Septuagesima) used elsewhere (see prime-opening-by-season.yml). Filed
|
||||
# under the same "lent" season key for now since neither is exercisable
|
||||
# until milestone 4 anyway, but don't assume they're the same cutover once
|
||||
# real season resolution exists.
|
||||
id: marian-antiphon-ave-regina-caelorum
|
||||
text:
|
||||
la: |
|
||||
Ave, Regína cælórum, Ave, Dómina Angelórum: Salve radix, salve porta, Ex qua mundo lux est orta: Gaude, Virgo gloriósa, Super omnes speciósa, Vale, o valde decóra, Et pro nobis Christum exóra.
|
||||
|
||||
V. Dignáre me laudáre te, Virgo sacráta.
|
||||
R. Da mihi virtútem contra hostes tuos.
|
||||
|
||||
Orémus.
|
||||
Concéde, miséricors Deus, fragilitáti nostræ præsídium; ut, qui sanctæ Dei Genetrícis memóriam ágimus; intercessiónis ejus auxílio, a nostris iniquitátibus resurgámus. Per eúmdem Christum Dóminum nostrum. Amen.
|
||||
en: |
|
||||
Hail, O Queen of heaven, enthroned! Hail, by Angels Mistress owned! Root of Jesse, Gate of morn, Whence the world's true Light was born: Glorious Virgin, joy to thee, Loveliest whom in heaven they see: Fairest thou, where all are fair, Plead with Christ our sins to spare.
|
||||
|
||||
V. Allow me to praise thee, holy Virgin.
|
||||
R. Give me strength against thine enemies.
|
||||
|
||||
Let us pray.
|
||||
Grant, O merciful God, to our weak natures thy protection, that we who commemorate the holy Mother of God may, by the help of her intercession, arise from our iniquities. Through the same Christ our Lord. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,26 @@
|
||||
# Verified against Divinum Officium (Latin/English Mariaant.txt,
|
||||
# [Paschalis]). Covers Eastertide, Ascensiontide, and Pentecost together as
|
||||
# one span (unlike the chapter-responsory/hymn-doxology tables, which treat
|
||||
# those three as distinct sub-seasons) — real practice doesn't switch the
|
||||
# Marian antiphon again until Trinity Sunday.
|
||||
id: marian-antiphon-regina-caeli
|
||||
text:
|
||||
la: |
|
||||
Regína cæli, lætáre, allelúja; Quia quem meruísti portáre, allelúja, Resurréxit, sicut dixit, allelúja: Ora pro nobis Deum, allelúja.
|
||||
|
||||
V. Gaude et lætáre, Virgo María, allelúja.
|
||||
R. Quia surréxit Dóminus vere, allelúja.
|
||||
|
||||
Orémus.
|
||||
Deus, qui per resurrectiónem Fílii tui, Dómini nostri Jesu Christi, mundum lætificáre dignátus es: præsta, quǽsumus; ut, per ejus Genetrícem Vírginem Maríam, perpétuæ capiámus gáudia vitæ. Per eúmdem Christum Dóminum nostrum. Amen.
|
||||
en: |
|
||||
O Queen of heaven rejoice! alleluia: For He whom thou didst merit to bear, alleluia, Hath arisen as he said, alleluia. Pray for us to God, alleluia.
|
||||
|
||||
V. Rejoice and be glad, O Virgin Mary, alleluia.
|
||||
R. Because the Lord is truly risen, alleluia.
|
||||
|
||||
Let us pray.
|
||||
O God, who gave joy to the world through the resurrection of thy Son, our Lord Jesus Christ; grant, we beseech thee, that through his Mother, the Virgin Mary, we may obtain the joys of everlasting life. Through the same Christ our Lord. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,26 @@
|
||||
# Verified against Divinum Officium (Latin/English Mariaant.txt,
|
||||
# [Postpentecost]). This is the per-annum default — "Postpentecost" is
|
||||
# exactly what this codebase's placeholder default season ("trinitytide")
|
||||
# refers to (see calendar/index.ts), so Salve Regina is the correct
|
||||
# fallback, not an arbitrary pick.
|
||||
id: marian-antiphon-salve-regina
|
||||
text:
|
||||
la: |
|
||||
Salve, Regína, mater misericórdiæ; vita, dulcédo et spes nóstra, salve. Ad te clamámus éxsules fílii Hevæ. Ad te suspirámus geméntes et flentes in hac lacrimárum valle. Eja ergo, advocáta nostra, illos tuos misericórdes óculos ad nos convérte. Et Jesum, benedíctum fructum ventris tui, nobis post hoc exsílium osténde. O clemens, o pia, o dulcis Virgo María.
|
||||
|
||||
V. Ora pro nobis, sancta Dei Génetrix.
|
||||
R. Ut digni efficiámur promissiónibus Christi.
|
||||
|
||||
Orémus.
|
||||
Omnípotens sempitérne Deus, qui gloriósæ Vírginis Matris Maríæ corpus et ánimam, ut dignum Fílii tui habitáculum éffici mererétur, Spíritu Sancto cooperánte, præparásti: da, ut, cujus commemoratióne lætámur, ejus pia intercessióne, ab instántibus malis et a morte perpétua liberémur. Per eúmdem Christum Dóminum nostrum. Amen.
|
||||
en: |
|
||||
Hail holy Queen, Mother of Mercy, our life, our sweetness, and our hope. To thee do we cry, poor banished children of Eve. To thee do we send up our sighs, mourning and weeping in this valley of tears. Turn then, most gracious Advocate, thine eyes of mercy toward us. And after this our exile show unto us the blessed fruit of thy womb, Jesus. O clement, O loving, O sweet Virgin Mary.
|
||||
|
||||
V. Pray for us, O Holy Mother of God.
|
||||
R. That we may be made worthy of the promises of Christ.
|
||||
|
||||
Let us pray.
|
||||
O almighty, everlasting God, who by the cooperation of the Holy Spirit, didst prepare the body and soul of Mary, glorious Virgin and Mother, to become a worthy dwelling for Thy Son; grant that we who rejoice in her commemoration may, by her gracious intercession, be delivered from present evils and from everlasting death. Through the same Christ our Lord. Amen.
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,12 @@
|
||||
# Verified against Divinum Officium (Tridentine 1906), 2026-08-25 run.
|
||||
# Fixed year-round (unlike Prime's weekday-variable psalm antiphon) — same
|
||||
# incipit/full split via the embedded "*", same by-rank opening logic (see
|
||||
# hours/antiphon.ts), full repeat after via its own 'closing-antiphon'-style
|
||||
# part in the ordo.
|
||||
id: nunc-dimittis-antiphon
|
||||
text:
|
||||
la: "Salva nos, * Dómine, vigilántes, custódi nos dormiéntes; ut vigilémus cum Christo, et requiescámus in pace."
|
||||
en: "Protect us, * Lord, while we are awake and safeguard us while we sleep; that we may keep watch with Christ, and rest in peace."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,26 @@
|
||||
# Verified against Divinum Officium (Tridentine 1906 run, 2026-08-25 —
|
||||
# Monastic Compline doesn't have this canticle at all; it's added from the
|
||||
# Tridentine 1906 form by explicit choice). The antiphon that frames it
|
||||
# ("Salva nos") is a separate proper — see nunc-dimittis-antiphon.yml.
|
||||
id: nunc-dimittis
|
||||
text:
|
||||
la: |
|
||||
Nunc dimíttis ✠ servum tuum, Dómine, * secúndum verbum tuum in pace:
|
||||
Quia vidérunt óculi mei * salutáre tuum,
|
||||
Quod parásti * ante fáciem ómnium populórum,
|
||||
Lumen ad revelatiónem géntium, * et glóriam plebis tuæ Israël.
|
||||
V. Glória Patri, et Fílio, * et Spirítui Sancto.
|
||||
R. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen.
|
||||
en: |
|
||||
Now ✠ thou dost dismiss thy servant, O Lord, * according to thy word in peace;
|
||||
Because my eyes have seen * thy salvation,
|
||||
Which thou hast prepared * before the face of all peoples:
|
||||
A light to the revelation of the Gentiles, * and the glory of thy people Israel.
|
||||
V. Glory be to the Father, and to the Son, * and to the Holy Ghost.
|
||||
R. As it was in the beginning, is now, * and ever shall be, world without end. Amen.
|
||||
citation:
|
||||
la: "Luc. 2:29-32"
|
||||
en: "Luke 2:29-32"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
# Verified against Divinum Officium (Monastic Tridentinum 1617), 2026-08-25
|
||||
# run. The opening "Deus in adiutorium" versicle, said aloud right after the
|
||||
# (dropped, see data/hours/prime.yml's note) silent Pater/Ave/Credo.
|
||||
# run. The opening "Deus in adiutorium" versicle — shared across every hour
|
||||
# that has one (Prime drops the silent Pater/Ave/Credo that would normally
|
||||
# precede it; Compline says it aloud right after its own pre-psalm Confiteor).
|
||||
#
|
||||
# This is the default (per-annum) ending — Septuagesima through Holy
|
||||
# Saturday swaps "Allelúja" for "Laus tibi..." instead; see
|
||||
# prime-opening-laus-tibi.yml and hours/opening-versicle.ts.
|
||||
id: prime-opening-alleluia
|
||||
# opening-laus-tibi.yml and hours/opening-versicle.ts.
|
||||
id: opening-alleluia
|
||||
text:
|
||||
la: |
|
||||
V. Deus ✠ in adjutórium meum inténde.
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
# Verified against Divinum Officium ("Psalterium/Common/Prayers.txt",
|
||||
# [Alleluia] section's 2nd line). Same opening versicle as
|
||||
# prime-opening-alleluia.yml, but with "Allelúja" replaced by "Laus tibi,
|
||||
# opening-alleluia.yml, but with "Allelúja" replaced by "Laus tibi,
|
||||
# Dómine, Rex ætérnæ glóriæ" — used Septuagesima through Holy Saturday
|
||||
# instead of the default. See hours/opening-versicle.ts.
|
||||
id: prime-opening-laus-tibi
|
||||
id: opening-laus-tibi
|
||||
text:
|
||||
la: |
|
||||
V. Deus ✠ in adjutórium meum inténde.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Verified against Divinum Officium (Latin Psalmorum, English translation).
|
||||
# Vulgate/Gallican numbering.
|
||||
number: 4
|
||||
verses:
|
||||
- n: 2
|
||||
text:
|
||||
la: "Cum invocárem exaudívit me Deus justítiæ meæ: * in tribulatióne dilatásti mihi."
|
||||
en: "When I called upon him, the God of my justice heard me: * when I was in distress, thou hast enlarged me."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 2
|
||||
text:
|
||||
la: "Miserére mei, * et exáudi oratiónem meam."
|
||||
en: "Have mercy on me: * and hear my prayer."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 3
|
||||
text:
|
||||
la: "Fílii hóminum, úsquequo gravi corde? * ut quid dilígitis vanitátem, et quǽritis mendácium?"
|
||||
en: "O ye sons of men, how long will you be dull of heart? * Why do you love vanity, and seek after lying?"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 4
|
||||
text:
|
||||
la: "Et scitóte quóniam mirificávit Dóminus sanctum suum: * Dóminus exáudiet me cum clamávero ad eum."
|
||||
en: "Know ye also that the Lord hath made his holy one wonderful: * the Lord will hear me when I shall cry unto him."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 5
|
||||
text:
|
||||
la: "Irascímini, et nolíte peccáre: ‡ quæ dícitis in córdibus vestris, * in cubílibus vestris compungímini."
|
||||
en: "Be ye angry, and sin not: ‡ the things you say in your hearts, * be sorry for them upon your beds."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 6
|
||||
text:
|
||||
la: "Sacrificáte sacrifícium justítiæ, † et speráte in Dómino. * Multi dicunt: Quis osténdit nobis bona?"
|
||||
en: "Offer up the sacrifice of justice, † and trust in the Lord: * many say, Who sheweth us good things?"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 7
|
||||
text:
|
||||
la: "Signátum est super nos lumen vultus tui, Dómine: * dedísti lætítiam in corde meo."
|
||||
en: "The light of thy countenance, O Lord, is signed upon us: * thou hast given gladness in my heart."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 8
|
||||
text:
|
||||
la: "A fructu fruménti, vini, et ólei sui * multiplicáti sunt."
|
||||
en: "By the fruit of their corn, their wine, and oil, * they are multiplied."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 9
|
||||
text:
|
||||
la: "In pace in idípsum * dórmiam, et requiéscam;"
|
||||
en: "In peace in the selfsame * I will sleep, and I will rest:"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 10
|
||||
text:
|
||||
la: "Quóniam tu, Dómine, singuláriter in spe * constituísti me."
|
||||
en: "For thou, O Lord, singularly * hast settled me in hope."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,116 @@
|
||||
# Verified against Divinum Officium (Latin Psalmorum, English translation).
|
||||
# Vulgate/Gallican numbering.
|
||||
number: 90
|
||||
verses:
|
||||
- n: 1
|
||||
text:
|
||||
la: "Qui hábitat in adjutório Altíssimi, * in protectióne Dei cæli commorábitur."
|
||||
en: "He that dwelleth in the aid of the most High, * shall abide under the protection of the God of Jacob."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 2
|
||||
text:
|
||||
la: "Dicet Dómino: Suscéptor meus es tu, et refúgium meum: * Deus meus sperábo in eum."
|
||||
en: "He shall say to the Lord: Thou art my protector, and my refuge: * my God, in him will I trust."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 3
|
||||
text:
|
||||
la: "Quóniam ipse liberávit me de láqueo venántium, * et a verbo áspero."
|
||||
en: "For he hath delivered me from the snare of the hunters: * and from the sharp word."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 4
|
||||
text:
|
||||
la: "Scápulis suis obumbrábit tibi: * et sub pennis ejus sperábis."
|
||||
en: "He will overshadow thee with his shoulders: * and under his wings thou shalt trust."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 5
|
||||
text:
|
||||
la: "Scuto circúmdabit te véritas ejus: * non timébis a timóre noctúrno,"
|
||||
en: "His truth shall compass thee with a shield: * thou shalt not be afraid of the terror of the night."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 6
|
||||
text:
|
||||
la: "A sagítta volánte in die, † a negótio perambulánte in ténebris: * ab incúrsu, et dæmónio meridiáno."
|
||||
en: "Of the arrow that flieth in the day, † of the business that walketh about in the dark: * of invasion, or of the noonday devil."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 7
|
||||
text:
|
||||
la: "Cadent a látere tuo mille, † et decem míllia a dextris tuis: * ad te autem non appropinquábit."
|
||||
en: "A thousand shall fall at thy side, † and ten thousand at thy right hand: * but it shall not come nigh thee."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 8
|
||||
text:
|
||||
la: "Verúmtamen óculis tuis considerábis: * et retributiónem peccatórum vidébis."
|
||||
en: "But thou shalt consider with thy eyes: * and shalt see the reward of the wicked."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 9
|
||||
text:
|
||||
la: "Quóniam tu es, Dómine, spes mea: * Altíssimum posuísti refúgium tuum."
|
||||
en: "Because thou, O Lord, art my hope: * thou hast made the most High thy refuge."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 10
|
||||
text:
|
||||
la: "Non accédet ad te malum: * et flagéllum non appropinquábit tabernáculo tuo."
|
||||
en: "There shall no evil come to thee: * nor shall the scourge come near thy dwelling."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 11
|
||||
text:
|
||||
la: "Quóniam Ángelis suis mandávit de te: * ut custódiant te in ómnibus viis tuis."
|
||||
en: "For he hath given his angels charge over thee; * to keep thee in all thy ways."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 12
|
||||
text:
|
||||
la: "In mánibus portábunt te: * ne forte offéndas ad lápidem pedem tuum."
|
||||
en: "In their hands they shall bear thee up: * lest thou dash thy foot against a stone."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 13
|
||||
text:
|
||||
la: "Super áspidem, et basilíscum ambulábis: * et conculcábis leónem et dracónem."
|
||||
en: "Thou shalt walk upon the asp and the basilisk: * and thou shalt trample under foot the lion and the dragon."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 14
|
||||
text:
|
||||
la: "Quóniam in me sperávit, liberábo eum: * prótegam eum, quóniam cognóvit nomen meum."
|
||||
en: "Because he hoped in me I will deliver him: * I will protect him because he hath known my name."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 15
|
||||
text:
|
||||
la: "Clamábit ad me, et ego exáudiam eum: ‡ cum ipso sum in tribulatióne: * erípiam eum et glorificábo eum."
|
||||
en: "He shall cry to me, and I will hear him: ‡ I am with him in tribulation, * I will deliver him, and I will glorify him."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 16
|
||||
text:
|
||||
la: "Longitúdine diérum replébo eum: * et osténdam illi salutáre meum."
|
||||
en: "I will fill him with length of days; * and I will shew him my salvation."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
@@ -0,0 +1,32 @@
|
||||
# Verified against Divinum Officium (Latin Psalmorum, English translation).
|
||||
# Vulgate/Gallican numbering.
|
||||
number: 133
|
||||
verses:
|
||||
- n: 1
|
||||
text:
|
||||
la: "Ecce nunc benedícite Dóminum, * omnes servi Dómini:"
|
||||
en: "Behold now bless ye the Lord, * all ye servants of the Lord:"
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 1
|
||||
text:
|
||||
la: "Qui statis in domo Dómini, * in átriis domus Dei nostri."
|
||||
en: "Who stand in the house of the Lord, * in the courts of the house of our God."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 2
|
||||
text:
|
||||
la: "In nóctibus extóllite manus vestras in sancta, * et benedícite Dóminum."
|
||||
en: "In the nights lift up your hands to the holy places, * and bless ye the Lord."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
- n: 3
|
||||
text:
|
||||
la: "Benedícat te Dóminus ex Sion, * qui fecit cælum et terram."
|
||||
en: "May the Lord out of Sion bless thee, * he that made heaven and earth."
|
||||
status:
|
||||
la: verified
|
||||
en: verified
|
||||
+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 }
|
||||
|
||||
+9
-2
@@ -96,14 +96,21 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
case 'canticle':
|
||||
case 'canticle': {
|
||||
const title = part.canticleId
|
||||
.split('-')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
return `
|
||||
<section class="ordo-part ordo-part-canticle">
|
||||
${part.antiphon ? renderColumns(part.antiphon, languages) : ''}
|
||||
<h3 class="ordo-part-label">Canticle</h3>
|
||||
<h3 class="ordo-part-label">${escapeHtml(title)}</h3>
|
||||
${renderCitation(part.text)}
|
||||
${renderColumns(part.text, languages)}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function renderHourView(container: HTMLElement): void {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { easterSunday } from '../../src/calendar/easter';
|
||||
import { toIsoDate } from '../../src/calendar/date-math';
|
||||
|
||||
// Reference dates are public record (Western/Gregorian Easter Sunday),
|
||||
// spanning different centuries and both early and late extremes.
|
||||
const KNOWN_EASTER_DATES: Record<number, string> = {
|
||||
1583: '1583-04-10', // first year the Gregorian algorithm is valid
|
||||
1900: '1900-04-15',
|
||||
1954: '1954-04-18',
|
||||
2000: '2000-04-23',
|
||||
2019: '2019-04-21',
|
||||
2020: '2020-04-12',
|
||||
2021: '2021-04-04',
|
||||
2022: '2022-04-17',
|
||||
2023: '2023-04-09',
|
||||
2024: '2024-03-31', // one of the earliest possible dates
|
||||
2025: '2025-04-20',
|
||||
2026: '2026-04-05',
|
||||
2027: '2027-03-28',
|
||||
2028: '2028-04-16',
|
||||
2038: '2038-04-25', // one of the latest possible dates
|
||||
2100: '2100-03-28',
|
||||
};
|
||||
|
||||
describe('easterSunday', () => {
|
||||
for (const [year, expected] of Object.entries(KNOWN_EASTER_DATES)) {
|
||||
it(`resolves ${year} to ${expected}`, () => {
|
||||
expect(toIsoDate(easterSunday(Number(year)))).toBe(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveSeason } from '../../src/calendar/temporal';
|
||||
|
||||
// Easter 2026 = Apr 5. Reference boundaries computed by hand from that.
|
||||
describe('resolveSeason', () => {
|
||||
it('resolves Advent (the Sunday nearest Nov 30) through Dec 24', () => {
|
||||
// Nov 30, 2025 is a Sunday -> Advent I starts that day.
|
||||
expect(resolveSeason('2025-11-29')).toBe('trinitytide');
|
||||
expect(resolveSeason('2025-11-30')).toBe('advent');
|
||||
expect(resolveSeason('2025-12-24')).toBe('advent');
|
||||
});
|
||||
|
||||
it('resolves Christmastide across the year boundary (Dec 25 - Jan 5)', () => {
|
||||
expect(resolveSeason('2025-12-25')).toBe('christmastide');
|
||||
expect(resolveSeason('2025-12-31')).toBe('christmastide');
|
||||
expect(resolveSeason('2026-01-01')).toBe('christmastide');
|
||||
expect(resolveSeason('2026-01-05')).toBe('christmastide');
|
||||
});
|
||||
|
||||
it('resolves Epiphanytide from Jan 6 until Septuagesima', () => {
|
||||
expect(resolveSeason('2026-01-06')).toBe('epiphanytide');
|
||||
// Easter 2026 is Apr 5, so Septuagesima (-63 days) is Feb 1.
|
||||
expect(resolveSeason('2026-01-31')).toBe('epiphanytide');
|
||||
});
|
||||
|
||||
it('resolves the Easter-offset seasons for 2026 (Easter = Apr 5)', () => {
|
||||
expect(resolveSeason('2026-02-01')).toBe('septuagesima'); // Easter - 63
|
||||
expect(resolveSeason('2026-02-18')).toBe('lent'); // Ash Wednesday, Easter - 46
|
||||
expect(resolveSeason('2026-03-22')).toBe('passiontide'); // Easter - 14
|
||||
expect(resolveSeason('2026-04-05')).toBe('eastertide'); // Easter Sunday
|
||||
expect(resolveSeason('2026-05-14')).toBe('ascensiontide'); // Easter + 39
|
||||
expect(resolveSeason('2026-05-24')).toBe('pentecost'); // Easter + 49
|
||||
expect(resolveSeason('2026-05-31')).toBe('trinitytide'); // Easter + 56
|
||||
});
|
||||
|
||||
it('resolves the Corpus Christi and Sacred Heart single-day overrides', () => {
|
||||
expect(resolveSeason('2026-06-04')).toBe('corpus-christi'); // Easter + 60
|
||||
expect(resolveSeason('2026-06-12')).toBe('sacred-heart'); // Easter + 68
|
||||
// The days immediately around them are still plain Trinitytide.
|
||||
expect(resolveSeason('2026-06-05')).toBe('trinitytide');
|
||||
});
|
||||
|
||||
it('resolves plain Trinitytide between Trinity Sunday and Advent', () => {
|
||||
expect(resolveSeason('2026-09-15')).toBe('trinitytide');
|
||||
});
|
||||
|
||||
it('never needs a cross-year Easter lookup for January dates', () => {
|
||||
// A year with a very early Easter (2027-03-28) still keeps Septuagesima
|
||||
// (Jan 24) safely after Epiphany (Jan 6).
|
||||
expect(resolveSeason('2027-01-06')).toBe('epiphanytide');
|
||||
expect(resolveSeason('2027-01-23')).toBe('epiphanytide');
|
||||
expect(resolveSeason('2027-01-24')).toBe('septuagesima');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveOrdo } from '../../src/hours';
|
||||
|
||||
const MONDAY = '2026-08-10';
|
||||
const SUNDAY = '2026-08-09';
|
||||
|
||||
describe('resolveOrdo("compline", ...)', () => {
|
||||
it('is implemented, with fixed psalms 4, 90, 133 regardless of weekday', () => {
|
||||
const monday = resolveOrdo('compline', MONDAY);
|
||||
const sunday = resolveOrdo('compline', SUNDAY);
|
||||
expect(monday.notImplemented).toBeUndefined();
|
||||
|
||||
const psalmNumbers = (ordo: typeof monday) =>
|
||||
ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(psalmNumbers(monday)).toEqual([4, 90, 133]);
|
||||
expect(psalmNumbers(sunday)).toEqual([4, 90, 133]);
|
||||
});
|
||||
|
||||
it('gives the psalms no antiphon (real ferial Compline has none)', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const psalms = ordo.parts.filter((p) => p.kind === 'psalm');
|
||||
for (const psalm of psalms) {
|
||||
expect(psalm.kind === 'psalm' ? psalm.antiphon : 'should be undefined').toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves the opening versicle and the pre-psalm Confiteor (before the psalms, unlike Prime)', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const firstPsalmIndex = ordo.parts.findIndex((p) => p.kind === 'psalm');
|
||||
const confiteorIndex = ordo.parts.findIndex(
|
||||
(p) => p.kind === 'preces' && p.text.text.la?.includes('Confíteor Deo'),
|
||||
);
|
||||
const openingIndex = ordo.parts.findIndex((p) => p.kind === 'versicle');
|
||||
expect(confiteorIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(openingIndex).toBeGreaterThan(confiteorIndex);
|
||||
expect(firstPsalmIndex).toBeGreaterThan(openingIndex);
|
||||
});
|
||||
|
||||
it("appends the hymn's own (per-annum) doxology, distinct from Prime's", () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
||||
expect(hymn && hymn.kind === 'hymn' ? hymn.text.text.en : undefined).toContain(
|
||||
'O Father, that we ask be done',
|
||||
);
|
||||
});
|
||||
|
||||
it('gives the capitulum its scripture citation (fixed, not Sunday/feria-split)', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
||||
expect(chapter && chapter.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Jer. 14:9');
|
||||
});
|
||||
|
||||
it('resolves the Nunc Dimittis as a canticle with an opening antiphon, plus a full antiphon reprise', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const canticle = ordo.parts.find((p) => p.kind === 'canticle');
|
||||
expect(canticle && canticle.kind === 'canticle' ? canticle.canticleId : undefined).toBe('nunc-dimittis');
|
||||
expect(canticle && canticle.kind === 'canticle' ? canticle.text.text.en : undefined).toContain(
|
||||
'Now ✠ thou dost dismiss thy servant',
|
||||
);
|
||||
expect(canticle && canticle.kind === 'canticle' ? canticle.antiphon?.text.en : undefined).toBe(
|
||||
'Ant. Protect us.',
|
||||
);
|
||||
|
||||
const canticleIndex = ordo.parts.indexOf(canticle!);
|
||||
const closingAntiphon = ordo.parts[canticleIndex + 1];
|
||||
expect(closingAntiphon?.kind).toBe('antiphon');
|
||||
// The "*" chant mark is preserved in the full (closing) form.
|
||||
expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.en : undefined).toBe(
|
||||
'Ant. Protect us, * Lord, while we are awake and safeguard us while we sleep; that we may keep watch with Christ, and rest in peace.',
|
||||
);
|
||||
});
|
||||
|
||||
it('includes the fuller Tridentine 1906 Preces (the triple "Blessed art thou" exchange Monastic drops)', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
|
||||
expect(preces && preces.kind === 'preces' ? preces.text.text.la : undefined).toContain('Benedíctus es, Dómine');
|
||||
expect(preces && preces.kind === 'preces' ? preces.text.text.la : undefined).toContain('Miserére nostri');
|
||||
});
|
||||
|
||||
it('closes with the Salve Regina (the per-annum default Marian antiphon), labeled by name', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const last = ordo.parts[ordo.parts.length - 1];
|
||||
expect(last?.kind).toBe('preces');
|
||||
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Salve Regina');
|
||||
expect(last?.kind === 'preces' ? last.text.text.en : undefined).toContain('Hail holy Queen');
|
||||
});
|
||||
|
||||
it('switches to the Ave Regina Caelorum from Candlemas through the day before Maundy Thursday', () => {
|
||||
const marianLabel = (date: string) => {
|
||||
const ordo = resolveOrdo('compline', date);
|
||||
const last = ordo.parts[ordo.parts.length - 1];
|
||||
return last?.kind === 'preces' ? last.label : undefined;
|
||||
};
|
||||
// Easter 2026 is Apr 5, so Maundy Thursday is Apr 2.
|
||||
expect(marianLabel('2026-02-01')).toBe('Salve Regina'); // day before Candlemas
|
||||
expect(marianLabel('2026-02-02')).toBe('Ave Regina Caelorum'); // Candlemas itself
|
||||
expect(marianLabel('2026-03-10')).toBe('Ave Regina Caelorum'); // mid-Lent
|
||||
expect(marianLabel('2026-04-01')).toBe('Ave Regina Caelorum'); // eve of Maundy Thursday
|
||||
expect(marianLabel('2026-04-02')).not.toBe('Ave Regina Caelorum'); // Maundy Thursday itself
|
||||
});
|
||||
|
||||
it('closes the Short Reading with "Deo gratias", via the shared lesson-close proper', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const shortReadingIndex = ordo.parts.findIndex((p) => p.kind === 'lesson' && p.label === 'Short Reading');
|
||||
const closing = ordo.parts[shortReadingIndex + 1];
|
||||
expect(closing?.kind).toBe('preces');
|
||||
expect(closing?.kind === 'preces' ? closing.text.text.en : undefined).toContain('Thanks be to God');
|
||||
});
|
||||
|
||||
it('includes the "In manus tuas" responsory between the capitulum and the versicle', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
const chapterIndex = ordo.parts.findIndex((p) => p.kind === 'chapter');
|
||||
const responsoryIndex = ordo.parts.findIndex((p) => p.kind === 'responsory');
|
||||
const versicleIndex = ordo.parts.findIndex((p) => p.kind === 'versicle' && p.text.text.la?.includes('Custódi nos'));
|
||||
expect(responsoryIndex).toBeGreaterThan(chapterIndex);
|
||||
expect(versicleIndex).toBeGreaterThan(responsoryIndex);
|
||||
const responsory = ordo.parts[responsoryIndex];
|
||||
expect(responsory?.kind === 'responsory' ? responsory.text.text.en : undefined).toContain(
|
||||
'Into thy hands, O Lord',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -188,8 +188,8 @@ describe('resolveOrdo("prime", ...)', () => {
|
||||
});
|
||||
|
||||
describe('resolveOrdo for not-yet-built hours', () => {
|
||||
it('flags compline as not implemented rather than throwing', () => {
|
||||
const ordo = resolveOrdo('compline', MONDAY);
|
||||
it('flags terce as not implemented rather than throwing', () => {
|
||||
const ordo = resolveOrdo('terce', MONDAY);
|
||||
expect(ordo.notImplemented).toBe(true);
|
||||
expect(ordo.parts).toEqual([]);
|
||||
});
|
||||
|
||||
+15
-1
@@ -41,13 +41,27 @@ describe('shell', () => {
|
||||
const root = document.getElementById('app')!;
|
||||
mountShell(root);
|
||||
|
||||
const terceBtn = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-hour]')).find(
|
||||
(btn) => btn.dataset.hour === 'terce',
|
||||
);
|
||||
terceBtn?.click();
|
||||
|
||||
expect(root.querySelector('.hour-view h2')?.textContent).toBe('Terce');
|
||||
expect(root.querySelector('.hour-view-pending')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('switches to Compline and shows its real content, not the pending message', () => {
|
||||
const root = document.getElementById('app')!;
|
||||
mountShell(root);
|
||||
|
||||
const complineBtn = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-hour]')).find(
|
||||
(btn) => btn.dataset.hour === 'compline',
|
||||
);
|
||||
complineBtn?.click();
|
||||
|
||||
expect(root.querySelector('.hour-view h2')?.textContent).toBe('Compline');
|
||||
expect(root.querySelector('.hour-view-pending')).not.toBeNull();
|
||||
expect(root.querySelector('.hour-view-pending')).toBeNull();
|
||||
expect(root.querySelectorAll('.ordo-part-psalm').length).toBe(3);
|
||||
});
|
||||
|
||||
it('moving to the next day updates the displayed date', () => {
|
||||
|
||||
Reference in New Issue
Block a user