// Matins readings for octave days — a separate small store from // common/temporal propers because these are keyed by // `${octaveId}-octave-day-${dayNumber}` (day 1 = the feast's own day, not // normally authored here since that day already has its own real content // elsewhere) rather than a flat id, and because unlike a collect, a // day's worth of historical lessons (usually 3, sometimes 9) is // deliberately collapsed into one continuous reading here rather than kept // broken up the way the sources present it — a project-specific // simplification, not how these are actually prayed historically. import type { LanguageCode, TranslationStatus } from '../psalter/types'; import { getScriptureVerses } from '../scripture'; /** A citation into scripture/index.ts's own store — `verses` e.g. "1-13", * omitted for a whole (authored) chapter. */ export interface ScriptureCitation { book: string; chapter: number; verses?: string; } export interface OctaveReadingText { id: string; /** Attribution for the reading — author and work, e.g. "St. John * Damascene, 2nd Sermon on the Dormition of the Mother of God". Bilingual * like everything else here since it's rendered as the reading's own * heading in both language columns (2026-08-21 fix: it used to be a bare * Latin incipit shown verbatim in the English column too). */ source: Partial>; text: Partial>; /** The first responsory following the reading in the source, where one * was found — which one to use when several lessons (and several * responsories) collapse into a single reading is an open question * (2026-08 discussion); "the first one" is the working answer. Omitting * the closing Gloria Patri outside Septuagesima-Holy Saturday is a * rendering concern, not captured here. */ responsory?: Partial>; status: Partial>; } /** * The shape actually authored in data/propers/octave-readings/*.yml: either * `text` directly (patristic prose that isn't scripture itself, embedded * per this project's usual duplication convention — 2026-08 decision: "we * actually want the scripture there in the way those readings cite it" for * quotes *within* prose, so those stay inline) or `passages`, a "pull this * passage" reference into the scripture store for readings that * are scripture start to finish — each entry becomes its own paragraph, in * order, letting a reading built from several verse ranges (e.g. three * consecutive Nocturn-I lessons) keep its original paragraph breaks. * Exactly one of the two is present; `getOctaveReading` always hands * callers a fully-resolved `OctaveReadingText` regardless of which was * used, so nothing downstream needs to know the difference. */ interface OctaveReadingRecord { id: string; source: Partial>; text?: Partial>; passages?: ScriptureCitation[]; responsory?: Partial>; status: Partial>; } /** Exported for reuse by src/propers/bible-plan.ts and * src/propers/nocturn-readings.ts, which need the same "pull several * scripture passages and join them as paragraphs" behavior for Matins * readings outside the octave-day-keyed store this file owns. */ export function resolvePassages(passages: ScriptureCitation[]): Partial> { const laParagraphs: string[] = []; const enParagraphs: string[] = []; for (const citation of passages) { const verses = getScriptureVerses(citation.book, citation.chapter, citation.verses); const la = verses .map((v) => (v.text.la ? `${v.n} ${v.text.la}` : undefined)) .filter((s): s is string => !!s) .join(' '); const en = verses .map((v) => (v.text.en ? `${v.n} ${v.text.en}` : undefined)) .filter((s): s is string => !!s) .join(' '); if (la) laParagraphs.push(la); if (en) enParagraphs.push(en); } const result: Partial> = {}; if (laParagraphs.length) result.la = laParagraphs.join('\n\n'); if (enParagraphs.length) result.en = enParagraphs.join('\n\n'); return result; } function resolveReading(record: OctaveReadingRecord): OctaveReadingText { const { passages, text, ...rest } = record; return { ...rest, text: text ?? (passages ? resolvePassages(passages) : {}) }; } const octaveReadingModules = import.meta.glob<{ default: OctaveReadingRecord }>( '../data/propers/octave-readings/*.yml', { eager: true }, ); const octaveReadingsById = new Map(); for (const mod of Object.values(octaveReadingModules)) { octaveReadingsById.set(mod.default.id, resolveReading(mod.default)); } /** Undefined, not a "missing" placeholder, when this octave day has no * reading authored — normal and expected (not every day has one), unlike * getCommonProper/getTemporalProper's ids, which are always expected to * resolve to *something* eventually. */ export function getOctaveReading(octaveId: string, dayNumber: number): OctaveReadingText | undefined { return octaveReadingsById.get(`${octaveId}-octave-day-${dayNumber}`); }