Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25934865b1 | |||
| 6a17a74b9b |
@@ -20,13 +20,6 @@ across every hour and content type in this app.
|
||||
1. **Mechanism gaps** — a real design/code piece not built yet, blocking any content pass on
|
||||
it from starting:
|
||||
- Easter's own octave — tabled, "a whole different discussion."
|
||||
- Gospel pericope + paired homily rendered as one part: decided, not yet built. When a
|
||||
Matins Nocturn 3 reading pairs a Gospel pericope with a patristic homily on it, render them
|
||||
as two distinct parts, one after the other, with markup that visibly distinguishes pericope
|
||||
from homily (not concatenated into a single `lesson` text as now). A bible-plan Gospel
|
||||
reading has no paired homily — it renders as just the pericope, which is correct as-is.
|
||||
Needs a schema change (splitting `lesson`+`isGospel` into a Gospel part with an optional
|
||||
homily part) and matching UI treatment in `hour-view.ts`.
|
||||
- Date picker: `day-nav.ts` currently only steps a day at a time (prev/next); there's no
|
||||
way to jump to an arbitrary date without walking there one day at a time or editing the
|
||||
URL. Needs a calendar-grid picker UI — open, not started.
|
||||
@@ -3887,3 +3880,38 @@ Transfiguration, All Saints of the Benedictine Order). The same session also clo
|
||||
pass touched (see "Matins hymn gap closed for the same categories/saints" elsewhere in this
|
||||
file) — Matins hymns are a different mechanism from the Lauds/Vespers bundle and needed their
|
||||
own pass.
|
||||
|
||||
### Gospel pericope + paired homily rendered as one part — done (2026-08-28)
|
||||
|
||||
Closed the last open Matins mechanism gap. `ResolvedPart` gained its own `'gospel'` kind
|
||||
(`src/hours/types.ts`), replacing the old `lesson`+`isGospel` flag entirely — a Gospel pericope
|
||||
now carries an optional nested `homily` (`{ source?, text }`) instead of being a same-kind
|
||||
sibling `lesson` part next to it.
|
||||
|
||||
The reason for nesting rather than keeping two adjacent pool entries: `distributeIntoNocturns`'s
|
||||
front-light 1/1/rest slicing (see "Reading distribution" elsewhere in this file) splits the
|
||||
reading pool by raw index, with no notion that two adjacent entries belong together — a Gospel
|
||||
pericope and its own homily, sourced as two separate array entries in
|
||||
`data/propers/nocturn-readings/*.yml`, could land in different nocturns purely by where they
|
||||
fell in the pool. Fixed at the pool-assembly step instead of patching the slicer:
|
||||
`buildReadingPool` (`src/hours/matins.ts`) now detects an `isGospel: true` NocturnReading
|
||||
immediately followed by a same-nocturn `isGospel: false` one and folds the pair into a single
|
||||
`gospel` pool entry before it ever reaches `distributeIntoNocturns` — the pairing is structural
|
||||
(one array element), not a convention the slicer has to know about. A bible-plan Gospel reading
|
||||
(never paired with a homily, per the user's own plan) becomes a bare `gospel` part with no
|
||||
`homily`, so it still gets the same "Gospel" UI treatment.
|
||||
|
||||
One real data wrinkle surfaced, not touched: `chair-of-st-peter-at-rome.yml`'s Nocturn 3 has the
|
||||
homily prose already folded into the same `isGospel: true` entry's own `text` (an earlier, less
|
||||
clean authoring convention flagged in this file's own SanctiM-pass notes) rather than as a
|
||||
separate following reading — that file is left as-is per the earlier decision not to "fix" it;
|
||||
the new `'gospel'` type's `source` field (distinct from `homily`) exists specifically to carry
|
||||
that entry's own attribution without misrepresenting it as a separate homily reading.
|
||||
|
||||
`hour-view.ts` renders the pericope with a "Gospel" heading, then the nested homily (when
|
||||
present) directly underneath in the same section, visually set off by a top border
|
||||
(`.ordo-part-homily` in `styles.css`) — one section, pericope then homily, rather than two
|
||||
separate `ordo-part` blocks that could be visually or structurally pulled apart.
|
||||
`tests/hours/matins.test.ts`'s Sunday proof (2026-09-06, Luke 7:11-16 + St. Augustine's homily)
|
||||
updated to assert against the new `gospel`/`homily` shape instead of two `isGospel`-flagged
|
||||
lessons. `npm test`/`npm run build` both pass.
|
||||
|
||||
+48
-10
@@ -515,11 +515,30 @@ function nocturnReadingPart(r: NocturnReading): ResolvedPart {
|
||||
kind: 'lesson',
|
||||
text: { text: r.text, status: r.status, citation: r.citation },
|
||||
label: r.source,
|
||||
isGospel: r.isGospel,
|
||||
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Builds a single atomic 'gospel' pool entry from a Gospel-flagged
|
||||
* NocturnReading, folding in `homily` (the immediately following reading in
|
||||
* the same array, when it's a genuine separate homily on this pericope —
|
||||
* see `buildReadingPool`'s own pairing check) as a nested field rather than
|
||||
* a second pool entry. This is what makes the pairing structural: a single
|
||||
* ResolvedPart can't be split across two nocturns by `distributeIntoNocturns`,
|
||||
* whereas two adjacent pool entries could be (and, before this, sometimes
|
||||
* were). */
|
||||
function gospelReadingPart(r: NocturnReading, homily: NocturnReading | undefined): ResolvedPart {
|
||||
return {
|
||||
kind: 'gospel',
|
||||
text: { text: r.text, status: r.status, citation: r.citation },
|
||||
source: r.source,
|
||||
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
||||
homily: homily
|
||||
? { source: homily.source, text: { text: homily.text, status: homily.status, citation: homily.citation } }
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** The full pool of readings available for `day` — every source that can
|
||||
* contribute (see this file's header): the user's own scripture-plan
|
||||
* readings, the office winner's and every commemorated saint's own
|
||||
@@ -538,12 +557,16 @@ function nocturnReadingPart(r: NocturnReading): ResolvedPart {
|
||||
function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string): ResolvedPart[] {
|
||||
const parts: ResolvedPart[] = [];
|
||||
for (const r of getBiblePlanReadings(temporalId, day.weekday, date)) {
|
||||
parts.push({
|
||||
kind: 'lesson',
|
||||
text: { text: r.text, status: r.status, citation: r.citation },
|
||||
isGospel: r.isGospel,
|
||||
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
||||
});
|
||||
const responsory = r.responsory ? { text: r.responsory, status: { la: 'verified' as const, en: 'verified' as const } } : undefined;
|
||||
// The user's own reading plan never pairs a Gospel with a homily (see
|
||||
// this file's own header) — a bare pericope, still its own 'gospel'
|
||||
// kind so it gets the same distinguishing UI treatment as a proper
|
||||
// Gospel+homily.
|
||||
parts.push(
|
||||
r.isGospel
|
||||
? { kind: 'gospel', text: { text: r.text, status: r.status, citation: r.citation }, responsory }
|
||||
: { kind: 'lesson', text: { text: r.text, status: r.status, citation: r.citation }, responsory },
|
||||
);
|
||||
}
|
||||
// Grouped by the reading's own `nocturn` tag (ascending), not by which id
|
||||
// contributed it — a later post-Pentecost Sunday's Nocturn 2 now comes
|
||||
@@ -558,9 +581,25 @@ function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string):
|
||||
const ids = nocturnReadingIds(day, temporalId, date);
|
||||
const byNocturn = new Map<number, ResolvedPart[]>();
|
||||
for (const id of ids) {
|
||||
for (const reading of getNocturnReadings(id)) {
|
||||
const readings = getNocturnReadings(id);
|
||||
for (let i = 0; i < readings.length; i++) {
|
||||
const reading = readings[i]!;
|
||||
const bucket = byNocturn.get(reading.nocturn) ?? [];
|
||||
bucket.push(nocturnReadingPart(reading));
|
||||
if (reading.isGospel) {
|
||||
// A genuine separate homily on this pericope is the very next
|
||||
// reading in the same file, in the same nocturn, not itself flagged
|
||||
// as a Gospel — the established authoring convention (see
|
||||
// src/propers/nocturn-readings.ts's own header and this file's
|
||||
// 'gospel' ResolvedPart doc comment). When found, fold it in and
|
||||
// skip it as its own pool entry so the pair can never be split
|
||||
// apart by distributeIntoNocturns.
|
||||
const next = readings[i + 1];
|
||||
const homily = next && !next.isGospel && next.nocturn === reading.nocturn ? next : undefined;
|
||||
bucket.push(gospelReadingPart(reading, homily));
|
||||
if (homily) i++;
|
||||
} else {
|
||||
bucket.push(nocturnReadingPart(reading));
|
||||
}
|
||||
byNocturn.set(reading.nocturn, bucket);
|
||||
}
|
||||
}
|
||||
@@ -574,7 +613,6 @@ function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string):
|
||||
kind: 'lesson',
|
||||
text: { text: reading.text, status: reading.status },
|
||||
label: reading.source,
|
||||
isGospel: false,
|
||||
responsory: reading.responsory
|
||||
? { text: reading.responsory, status: { la: 'verified', en: 'verified' } }
|
||||
: undefined,
|
||||
|
||||
+32
-11
@@ -247,24 +247,45 @@ export type ResolvedPart =
|
||||
// readings' own `source` is a genuine bilingual attribution (their
|
||||
// English title is a real translation of a Latin incipit, not an
|
||||
// editorial description written once) so `label` accepts either shape —
|
||||
// see hour-view.ts's renderLessonLabel. `isGospel` flags a reading
|
||||
// as a Gospel pericope (from either the user's own continuous-reading
|
||||
// plan or the day's own proper Gospel+homily — never a Common-of-Saints
|
||||
// fallback Gospel, which this app deliberately excludes for Matins, see
|
||||
// hours/matins.ts) rather than a distinct part kind, since a Gospel
|
||||
// reading is still fundamentally a lesson — just one worth marking.
|
||||
// `responsory` is the short responsory said after the reading, when one
|
||||
// was sourced (matched loosely by scriptural book for the user's own
|
||||
// plan, live-queried directly for patristic/hagiographic readings) —
|
||||
// absent, not fabricated, when none could be sourced.
|
||||
// see hour-view.ts's renderLessonLabel. `responsory` is the short
|
||||
// responsory said after the reading, when one was sourced (matched
|
||||
// loosely by scriptural book for the user's own plan, live-queried
|
||||
// directly for patristic/hagiographic readings) — absent, not
|
||||
// fabricated, when none could be sourced. A Gospel pericope is never
|
||||
// rendered with this kind — see 'gospel' below.
|
||||
| {
|
||||
kind: 'lesson';
|
||||
text: ResolvedText;
|
||||
label?: string | Partial<Record<string, string>>;
|
||||
nocturn?: number;
|
||||
source?: string;
|
||||
isGospel?: boolean;
|
||||
responsory?: ResolvedText;
|
||||
}
|
||||
// Matins only. A Gospel pericope, from either the user's own continuous
|
||||
// reading plan (no paired homily) or the day's own genuine proper
|
||||
// Gospel+homily (src/propers/nocturn-readings.ts — never a
|
||||
// Common-of-Saints fallback Gospel, which this app deliberately excludes
|
||||
// for Matins, see hours/matins.ts). Split out from the generic 'lesson'
|
||||
// kind (2026-08-28) so a Gospel and its paired homily can be modeled as
|
||||
// one atomic part: when the source pairs a pericope with a patristic
|
||||
// homily on it as two adjacent readings, hours/matins.ts's
|
||||
// buildReadingPool combines them into a single 'gospel' part with
|
||||
// `homily` nested, rather than two separate pool entries — this is what
|
||||
// guarantees distributeIntoNocturns can never split a Gospel from its
|
||||
// own homily across two different nocturns; the pool's earlier
|
||||
// front-light 1/1/rest slicing could otherwise do exactly that. `source`
|
||||
// is set only for the rare case where the pericope's own text already
|
||||
// has homily prose folded into it at the data layer (an earlier, less
|
||||
// clean authoring convention — see chair-of-st-peter-at-rome.yml's own
|
||||
// header) — a genuinely separate homily reading, when authored, is
|
||||
// `homily` instead.
|
||||
| {
|
||||
kind: 'gospel';
|
||||
text: ResolvedText;
|
||||
nocturn?: number;
|
||||
source?: string;
|
||||
responsory?: ResolvedText;
|
||||
homily?: { source?: string; text: ResolvedText };
|
||||
};
|
||||
|
||||
export interface ResolvedVerse {
|
||||
|
||||
+18
-6
@@ -1,6 +1,7 @@
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveEveningDay } from '../calendar/vespers';
|
||||
import { weekdayOf } from '../calendar/weekday';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
@@ -70,9 +71,16 @@ function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] {
|
||||
/** The 4 weekday-variable psalm groups — no fixed leading/trailing psalm
|
||||
* the way Lauds has (Ps 66 / the Laudate psalms); Vespers is just these 4
|
||||
* groups in sequence. No per-feast override mechanism yet — see
|
||||
* hours/types.ts's 'vespers-psalmody' doc comment. */
|
||||
function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
||||
const wd = vespersAntiphons[day.weekday];
|
||||
* hours/types.ts's 'vespers-psalmody' doc comment. Deliberately keyed off
|
||||
* `weekday` (today's actual calendar weekday), not `day.weekday` — `day`
|
||||
* may be tomorrow's identity when tonight anticipates First Vespers (see
|
||||
* resolveEveningDay), but the psalm cycle has no per-feast override and
|
||||
* must stay on today's place in the fixed 6-day rotation regardless of
|
||||
* whose office governs the evening's propers. `day` is still threaded
|
||||
* into psalmParts for antiphon-fullness resolution, which does care who
|
||||
* governs tonight. */
|
||||
function resolvePsalmody(day: LiturgicalDay, weekday: Weekday): ResolvedPart[] {
|
||||
const wd = vespersAntiphons[weekday];
|
||||
return wd.groups.flatMap((group) => psalmParts(group, day));
|
||||
}
|
||||
|
||||
@@ -154,12 +162,12 @@ function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
||||
];
|
||||
}
|
||||
|
||||
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
function resolvePart(part: HourPart, day: LiturgicalDay, weekday: Weekday): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'opening-versicle':
|
||||
return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }];
|
||||
case 'vespers-psalmody':
|
||||
return resolvePsalmody(day);
|
||||
return resolvePsalmody(day, weekday);
|
||||
case 'vespers-office':
|
||||
return resolveOffice(day);
|
||||
case 'magnificat': {
|
||||
@@ -216,6 +224,10 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
// vespers.ts) — getDayCollects picks that up for free, no special
|
||||
// handling needed here.
|
||||
const day = resolveEveningDay(date);
|
||||
const parts = vespersDefinition.parts.flatMap((part) => resolvePart(part, day));
|
||||
// Today's own weekday, independent of the possibly-anticipated `day`
|
||||
// above — see resolvePsalmody's doc comment for why psalm selection
|
||||
// must not follow the evening's borrowed identity.
|
||||
const weekday = weekdayOf(date);
|
||||
const parts = vespersDefinition.parts.flatMap((part) => resolvePart(part, day, weekday));
|
||||
return { hourId: 'vespers', date, parts, dayLabel: getDayLabel(day) };
|
||||
}
|
||||
|
||||
+30
-2
@@ -91,13 +91,41 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
||||
`;
|
||||
case 'lesson':
|
||||
return `
|
||||
<section class="ordo-part ordo-part-lesson${part.isGospel ? ' ordo-part-lesson-gospel' : ''}">
|
||||
${part.isGospel ? '<h3 class="ordo-part-label">Gospel</h3>' : renderLessonLabel(part.label, languages)}
|
||||
<section class="ordo-part ordo-part-lesson">
|
||||
${renderLessonLabel(part.label, languages)}
|
||||
${renderCitation(part.text)}
|
||||
${renderColumns(part.text, languages)}
|
||||
${part.responsory ? `<div class="ordo-part-responsory">${renderColumns(part.responsory, languages)}</div>` : ''}
|
||||
</section>
|
||||
`;
|
||||
// A Gospel pericope, with its paired patristic homily (when authored)
|
||||
// nested right after it in the same section rather than as a separate
|
||||
// pool entry — see hours/matins.ts's buildReadingPool, which combines
|
||||
// the two into one atomic part specifically so they can never end up in
|
||||
// different nocturns. `part.source` (rare — see the 'gospel' type's own
|
||||
// doc comment) is homily attribution already folded into the pericope's
|
||||
// own text at the data layer; `part.homily`, when present, is a
|
||||
// genuinely separate reading, rendered with its own label underneath.
|
||||
case 'gospel':
|
||||
return `
|
||||
<section class="ordo-part ordo-part-lesson ordo-part-gospel">
|
||||
<h3 class="ordo-part-label">Gospel</h3>
|
||||
${renderCitation(part.text)}
|
||||
${renderColumns(part.text, languages)}
|
||||
${part.source ? `<p class="ordo-part-citation">${escapeHtml(part.source)}</p>` : ''}
|
||||
${part.responsory ? `<div class="ordo-part-responsory">${renderColumns(part.responsory, languages)}</div>` : ''}
|
||||
${
|
||||
part.homily
|
||||
? `
|
||||
<div class="ordo-part-homily">
|
||||
${renderLessonLabel(part.homily.source, languages)}
|
||||
${renderColumns(part.homily.text, languages)}
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
case 'psalm': {
|
||||
// Always cite the full range, not just the psalm number — a 4-verse
|
||||
// slice of Psalm 118 is a different citation from the whole psalm.
|
||||
|
||||
@@ -307,6 +307,17 @@ button:focus-visible {
|
||||
margin: 0 0 var(--space-1);
|
||||
}
|
||||
|
||||
.ordo-part-gospel {
|
||||
border-left: 2px solid var(--color-brass);
|
||||
padding-left: var(--space-2);
|
||||
}
|
||||
|
||||
.ordo-part-homily {
|
||||
margin-top: var(--space-2);
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--color-brass);
|
||||
}
|
||||
|
||||
.psalm-verses {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
+17
-19
@@ -146,35 +146,33 @@ describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => {
|
||||
expect(citations).toContain('Sir 45');
|
||||
});
|
||||
|
||||
it('includes the real Nocturn 2 patristic reading (Gregory on Job) and Nocturn 3 Gospel + homily, both flagged correctly', () => {
|
||||
const lessons = ordo.parts.filter(
|
||||
(p) => p.kind === 'lesson',
|
||||
) as { label?: string; isGospel?: boolean; text: { text: { la?: string } } }[];
|
||||
it('includes the real Nocturn 2 patristic reading (Gregory on Job) and Nocturn 3 Gospel + homily as one atomic gospel part', () => {
|
||||
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { label?: string }[];
|
||||
const gregory = lessons.find((l) => l.label?.includes('Gregory'));
|
||||
expect(gregory).toBeDefined();
|
||||
expect(gregory?.isGospel).toBe(false);
|
||||
|
||||
const gospel = lessons.find((l) => l.isGospel);
|
||||
expect(gospel).toBeDefined();
|
||||
expect(gospel?.text.text.la).toContain('Naim');
|
||||
|
||||
const homily = lessons.find((l) => l.label?.includes('Augustine'));
|
||||
expect(homily).toBeDefined();
|
||||
expect(homily?.isGospel).toBe(false);
|
||||
const gospels = ordo.parts.filter((p) => p.kind === 'gospel') as {
|
||||
text: { text: { la?: string } };
|
||||
homily?: { source?: string; text: { text: { la?: string } } };
|
||||
}[];
|
||||
expect(gospels).toHaveLength(1);
|
||||
const gospel = gospels[0]!;
|
||||
expect(gospel.text.text.la).toContain('Naim');
|
||||
expect(gospel.homily?.source).toContain('Augustine');
|
||||
expect(gospel.homily?.text.text.la).toBeTruthy();
|
||||
});
|
||||
|
||||
it('never sources a Gospel from a Common-of-Saints fallback — every Gospel-flagged reading traces to the bible-plan or a real proper', () => {
|
||||
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { isGospel?: boolean; label?: string }[];
|
||||
const gospels = lessons.filter((l) => l.isGospel);
|
||||
it('never sources a Gospel from a Common-of-Saints fallback — the only gospel part traces to the bible-plan or a real proper', () => {
|
||||
const gospels = ordo.parts.filter((p) => p.kind === 'gospel');
|
||||
// This Sunday's own TSV row deliberately has no Gospel (confirmed
|
||||
// editorial choice) — the only Gospel-flagged reading should be the
|
||||
// day's own proper Nocturn 3 Gospel, not a substituted Common one.
|
||||
// editorial choice) — the only gospel part should be the day's own
|
||||
// proper Nocturn 3 Gospel, not a substituted Common one.
|
||||
expect(gospels).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('orders Te Deum after every reading, and the day collect last', () => {
|
||||
it('orders Te Deum after every reading (lesson or gospel), and the day collect last', () => {
|
||||
const teDeumIndex = ordo.parts.findIndex((p) => p.kind === 'te-deum');
|
||||
const lastLessonIndex = ordo.parts.map((p) => p.kind).lastIndexOf('lesson');
|
||||
const lastLessonIndex = ordo.parts.map((p) => p.kind).map((k, i) => ((k === 'lesson' || k === 'gospel') ? i : -1)).reduce((a, b) => Math.max(a, b), -1);
|
||||
const prayerIndex = ordo.parts.findIndex((p) => p.kind === 'prayer');
|
||||
expect(teDeumIndex).toBeGreaterThan(lastLessonIndex);
|
||||
expect(prayerIndex).toBeGreaterThan(teDeumIndex);
|
||||
|
||||
@@ -235,6 +235,20 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps ferial psalmody on the actual calendar weekday even while the office anticipates tomorrow\'s First Vespers', () => {
|
||||
// 2026-08-25 is a Tuesday, but its evening anticipates St. Zephyrinus's
|
||||
// First Vespers (Aug 26) per the test just above -- the psalm cycle has
|
||||
// no per-feast override (see hours/vespers.ts's resolvePsalmody), so it
|
||||
// must stay on Tuesday's own group [129, 130, 131, 132], not slip to
|
||||
// Wednesday's [134, 135, 136, 137] just because the office borrowed
|
||||
// Wednesday's identity.
|
||||
const ordo = resolveOrdo('vespers', '2026-08-25');
|
||||
const psalmNumbers = ordo.parts
|
||||
.filter((p) => p.kind === 'psalm')
|
||||
.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(psalmNumbers).toEqual([129, 130, 131, 132]);
|
||||
});
|
||||
|
||||
it("uses Common-of-Several-Martyrs' real office bundle on Ss. Placid and Companions' own day (Duplex II. classis, keeps own Vespers)", () => {
|
||||
// Live-verified against Divinum Officium (Monastic Tridentinum 1617)
|
||||
// votive=C3 -- see vespers-hymn-common-of-several-martyrs.yml's header.
|
||||
|
||||
Reference in New Issue
Block a user