Replace generic "Reading"/"Gospel" headings with real incipits
Deploy / deploy (push) Successful in 1m50s
Deploy / deploy (push) Successful in 1m50s
Bible-plan readings now carry a generated bilingual incipit label (bible-book-incipits.ts) instead of falling through to the UI's generic "Reading" heading. Gospel pericopes (whether from the user's own plan or nocturn-readings.ts's proper content) get the same treatment via a new label field on the 'gospel' ResolvedPart, recovering the book from the reading's own citation when no raw book code is available. Updates 3 matins.test.ts assertions that used "no label" as a stand-in for "this is a bible-plan reading" — no longer true now that these readings carry a real label, so they now key off the label's shape (string = patristic attribution, object = generated incipit) instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
+6
-2
@@ -92,6 +92,7 @@ import { getSaintRecord } from '../calendar/feasts';
|
|||||||
import { getBiblePlanReadings } from '../propers/bible-plan';
|
import { getBiblePlanReadings } from '../propers/bible-plan';
|
||||||
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
|
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
|
||||||
import { getOctaveReading } from '../propers/octave-readings';
|
import { getOctaveReading } from '../propers/octave-readings';
|
||||||
|
import { getGospelIncipitFromCitation } from '../propers/bible-book-incipits';
|
||||||
import { getMatinsSaintOverride, getMatinsCommonOverride, type MatinsPsalmodyScheme } from './matins-psalmody-overrides';
|
import { getMatinsSaintOverride, getMatinsCommonOverride, type MatinsPsalmodyScheme } from './matins-psalmody-overrides';
|
||||||
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
||||||
import matinsSundayNocturn3OverridesData from '../data/hours/matins-sunday-nocturn3-overrides.yml';
|
import matinsSundayNocturn3OverridesData from '../data/hours/matins-sunday-nocturn3-overrides.yml';
|
||||||
@@ -683,10 +684,12 @@ function nocturnReadingPart(r: NocturnReading): ResolvedPart {
|
|||||||
* whereas two adjacent pool entries could be (and, before this, sometimes
|
* whereas two adjacent pool entries could be (and, before this, sometimes
|
||||||
* were). */
|
* were). */
|
||||||
function gospelReadingPart(r: NocturnReading, homily: NocturnReading | undefined): ResolvedPart {
|
function gospelReadingPart(r: NocturnReading, homily: NocturnReading | undefined): ResolvedPart {
|
||||||
|
const incipit = getGospelIncipitFromCitation(r.citation);
|
||||||
return {
|
return {
|
||||||
kind: 'gospel',
|
kind: 'gospel',
|
||||||
text: { text: r.text, status: r.status, citation: r.citation },
|
text: { text: r.text, status: r.status, citation: r.citation },
|
||||||
source: r.source,
|
source: r.source,
|
||||||
|
label: incipit ? { la: incipit.la, en: incipit.en } : undefined,
|
||||||
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
||||||
homily: homily
|
homily: homily
|
||||||
? { source: homily.source, text: { text: homily.text, status: homily.status, citation: homily.citation } }
|
? { source: homily.source, text: { text: homily.text, status: homily.status, citation: homily.citation } }
|
||||||
@@ -717,10 +720,11 @@ function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string,
|
|||||||
// this file's own header) — a bare pericope, still its own 'gospel'
|
// this file's own header) — a bare pericope, still its own 'gospel'
|
||||||
// kind so it gets the same distinguishing UI treatment as a proper
|
// kind so it gets the same distinguishing UI treatment as a proper
|
||||||
// Gospel+homily.
|
// Gospel+homily.
|
||||||
|
const label = r.label ? { la: r.label.la ?? '', en: r.label.en ?? '' } : undefined;
|
||||||
parts.push(
|
parts.push(
|
||||||
r.isGospel
|
r.isGospel
|
||||||
? { kind: 'gospel', text: { text: r.text, status: r.status, citation: r.citation }, responsory }
|
? { kind: 'gospel', text: { text: r.text, status: r.status, citation: r.citation }, responsory, label }
|
||||||
: { kind: 'lesson', text: { text: r.text, status: r.status, citation: r.citation }, responsory },
|
: { kind: 'lesson', text: { text: r.text, status: r.status, citation: r.citation }, responsory, label },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Grouped by the reading's own `nocturn` tag (ascending), not by which id
|
// Grouped by the reading's own `nocturn` tag (ascending), not by which id
|
||||||
|
|||||||
@@ -297,6 +297,12 @@ export type ResolvedPart =
|
|||||||
text: ResolvedText;
|
text: ResolvedText;
|
||||||
nocturn?: number;
|
nocturn?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
/** The traditional "Léctio sancti Evangélii secúndum..."/"A reading
|
||||||
|
* from the Holy Gospel according to..." incipit, shown in place of the
|
||||||
|
* generic "Gospel" heading — see hour-view.ts's renderLessonLabel and
|
||||||
|
* bible-book-incipits.ts. Absent only when the pericope's own book
|
||||||
|
* couldn't be identified. */
|
||||||
|
label?: string | Partial<Record<string, string>>;
|
||||||
responsory?: ResolvedText;
|
responsory?: ResolvedText;
|
||||||
homily?: { source?: string; text: ResolvedText };
|
homily?: { source?: string; text: ResolvedText };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import type { LanguageCode, TranslationStatus } from '../psalter/types';
|
import type { LanguageCode, TranslationStatus } from '../psalter/types';
|
||||||
import { resolvePassages, type ScriptureCitation } from './octave-readings';
|
import { resolvePassages, type ScriptureCitation } from './octave-readings';
|
||||||
import { getResponsoryForBook } from './matins-responsories';
|
import { getResponsoryForBook } from './matins-responsories';
|
||||||
|
import { getBookIncipit } from './bible-book-incipits';
|
||||||
|
|
||||||
const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']);
|
const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']);
|
||||||
|
|
||||||
@@ -37,6 +38,11 @@ export interface BiblePlanReading {
|
|||||||
* data/hours/matins-responsories-by-book.yml — undefined when nothing's
|
* data/hours/matins-responsories-by-book.yml — undefined when nothing's
|
||||||
* seeded for that book yet, an honest absence, not a placeholder. */
|
* seeded for that book yet, an honest absence, not a placeholder. */
|
||||||
responsory?: Partial<Record<LanguageCode, string>>;
|
responsory?: Partial<Record<LanguageCode, string>>;
|
||||||
|
/** The traditional "Léctio libri..."/"A reading from..." incipit for the
|
||||||
|
* reading's own (first) book — see bible-book-incipits.ts. Undefined only
|
||||||
|
* for an abbreviation that map doesn't recognize, which shouldn't happen
|
||||||
|
* for anything actually under src/data/scripture. */
|
||||||
|
label?: Partial<Record<LanguageCode, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BiblePlanReadingRecord {
|
interface BiblePlanReadingRecord {
|
||||||
@@ -107,15 +113,17 @@ function resolveReading(record: BiblePlanReadingRecord, bookIndex: number): Bibl
|
|||||||
la: text.la ? 'verified' : 'missing',
|
la: text.la ? 'verified' : 'missing',
|
||||||
en: text.en ? 'verified' : 'missing',
|
en: text.en ? 'verified' : 'missing',
|
||||||
};
|
};
|
||||||
const label = formatCitationLabel(record.passages);
|
const citationLabelText = formatCitationLabel(record.passages);
|
||||||
const firstBook = record.passages[0]?.book;
|
const firstBook = record.passages[0]?.book;
|
||||||
const responsory = firstBook ? getResponsoryForBook(firstBook, bookIndex)?.text : undefined;
|
const responsory = firstBook ? getResponsoryForBook(firstBook, bookIndex)?.text : undefined;
|
||||||
|
const incipit = firstBook ? getBookIncipit(firstBook) : undefined;
|
||||||
return {
|
return {
|
||||||
text,
|
text,
|
||||||
status,
|
status,
|
||||||
citation: { la: label, en: label },
|
citation: { la: citationLabelText, en: citationLabelText },
|
||||||
isGospel: isGospelReading(record.passages),
|
isGospel: isGospelReading(record.passages),
|
||||||
responsory,
|
responsory,
|
||||||
|
label: incipit ? { la: incipit.la, en: incipit.en } : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -49,9 +49,10 @@ function renderCitation(text: ResolvedText, languages: readonly string[]): strin
|
|||||||
function renderLessonLabel(
|
function renderLessonLabel(
|
||||||
label: string | Partial<Record<string, string>> | undefined,
|
label: string | Partial<Record<string, string>> | undefined,
|
||||||
languages: readonly string[],
|
languages: readonly string[],
|
||||||
|
fallback = 'Reading',
|
||||||
): string {
|
): string {
|
||||||
if (!label) {
|
if (!label) {
|
||||||
return '<h3 class="ordo-part-label">Reading</h3>';
|
return `<h3 class="ordo-part-label">${escapeHtml(fallback)}</h3>`;
|
||||||
}
|
}
|
||||||
if (typeof label === 'string') {
|
if (typeof label === 'string') {
|
||||||
return `<h3 class="ordo-part-label">${escapeHtml(label)}</h3>`;
|
return `<h3 class="ordo-part-label">${escapeHtml(label)}</h3>`;
|
||||||
@@ -115,7 +116,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
|
|||||||
case 'gospel':
|
case 'gospel':
|
||||||
return `
|
return `
|
||||||
<section class="ordo-part ordo-part-lesson ordo-part-gospel">
|
<section class="ordo-part ordo-part-lesson ordo-part-gospel">
|
||||||
<h3 class="ordo-part-label">Gospel</h3>
|
${renderLessonLabel(part.label, languages, 'Gospel')}
|
||||||
${renderCitation(part.text, languages)}
|
${renderCitation(part.text, languages)}
|
||||||
${renderColumns(part.text, languages)}
|
${renderColumns(part.text, languages)}
|
||||||
${part.source ? `<p class="ordo-part-citation">${escapeHtml(part.source)}</p>` : ''}
|
${part.source ? `<p class="ordo-part-citation">${escapeHtml(part.source)}</p>` : ''}
|
||||||
|
|||||||
+15
-12
@@ -73,14 +73,17 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
||||||
// Filtered to unlabeled lessons only: since the temporal-cycle
|
// Filtered to the user's own bible-plan lessons only: since the
|
||||||
// nocturn-readings sweep authored advent-1.yml (2026-08), this same
|
// temporal-cycle nocturn-readings sweep authored advent-1.yml
|
||||||
// date's Nocturn 2/3 pool also gains two labeled (patristic) lesson
|
// (2026-08), this same date's Nocturn 2/3 pool also gains two labeled
|
||||||
// entries alongside the user's own bible-plan readings — a real,
|
// (patristic) lesson entries alongside the user's own bible-plan
|
||||||
// separate content source this test isn't about (see
|
// readings — a real, separate content source this test isn't about.
|
||||||
// hours/matins.ts's own nocturnReadingPart, which is the only lesson
|
// Bible-plan lessons carry a generated bilingual incipit `label`
|
||||||
// constructor that sets `label`).
|
// object (bible-book-incipits.ts); patristic ones carry a plain-string
|
||||||
const lessons = ordo.parts.filter((p) => p.kind === 'lesson' && !p.label);
|
// `label` (nocturnReadingPart's own `r.source` attribution) — that
|
||||||
|
// shape difference, not label presence/absence, is what distinguishes
|
||||||
|
// them since 2026-09-03.
|
||||||
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson' && typeof p.label !== 'string');
|
||||||
// Plain 2 bible-plan readings on this particular clean ferial (no
|
// Plain 2 bible-plan readings on this particular clean ferial (no
|
||||||
// commemorated saint here) — see the next test for the 3-reading case.
|
// commemorated saint here) — see the next test for the 3-reading case.
|
||||||
expect(lessons).toHaveLength(2);
|
expect(lessons).toHaveLength(2);
|
||||||
@@ -100,9 +103,9 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
|
|||||||
// Semiduplex threshold). 2025-12-02, a Tuesday, keeps the same 9-psalm
|
// Semiduplex threshold). 2025-12-02, a Tuesday, keeps the same 9-psalm
|
||||||
// ferial shape as the other tests in this block.
|
// ferial shape as the other tests in this block.
|
||||||
const commemoratedOrdo = resolveOrdo('matins', '2025-12-02');
|
const commemoratedOrdo = resolveOrdo('matins', '2025-12-02');
|
||||||
// Same unlabeled-lesson filter as the test above — advent-1.yml's
|
// Same bible-plan-lesson filter as the test above — advent-1.yml's
|
||||||
// patristic content also pools into this date.
|
// patristic content also pools into this date.
|
||||||
const lessons = commemoratedOrdo.parts.filter((p) => p.kind === 'lesson' && !p.label);
|
const lessons = commemoratedOrdo.parts.filter((p) => p.kind === 'lesson' && typeof p.label !== 'string');
|
||||||
expect(lessons).toHaveLength(3);
|
expect(lessons).toHaveLength(3);
|
||||||
const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en);
|
const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en);
|
||||||
expect(citations).toEqual(['Isa 6-7', 'Sap 2', undefined]);
|
expect(citations).toEqual(['Isa 6-7', 'Sap 2', undefined]);
|
||||||
@@ -159,8 +162,8 @@ describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('includes the real Nocturn 2 patristic reading (Gregory on Job) and Nocturn 3 Gospel + homily as one atomic gospel part', () => {
|
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 lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { label?: string | Record<string, string> }[];
|
||||||
const gregory = lessons.find((l) => l.label?.includes('Gregory'));
|
const gregory = lessons.find((l) => typeof l.label === 'string' && l.label.includes('Gregory'));
|
||||||
expect(gregory).toBeDefined();
|
expect(gregory).toBeDefined();
|
||||||
|
|
||||||
const gospels = ordo.parts.filter((p) => p.kind === 'gospel') as {
|
const gospels = ordo.parts.filter((p) => p.kind === 'gospel') as {
|
||||||
|
|||||||
Reference in New Issue
Block a user