Model a Matins Gospel pericope and its paired homily as one atomic part
Deploy / deploy (push) Successful in 1m25s
Deploy / deploy (push) Successful in 1m25s
Replaces the lesson+isGospel flag with a dedicated 'gospel' ResolvedPart carrying an optional nested homily. Previously a Gospel pericope and its patristic homily were two separate pool entries, which the front-light 1/1/rest nocturn slicer could split into different nocturns purely by index. buildReadingPool now folds an isGospel:true reading and its immediately-following same-nocturn homily into a single pool entry, so the pairing is structural rather than a convention the slicer has to honor. hour-view.ts renders the pair as one visually grouped section. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
This commit is contained in:
+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 {
|
||||
|
||||
+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;
|
||||
|
||||
Reference in New Issue
Block a user