import { getState } from '../app-state'; import { resolveOrdo } from '../hours'; import type { ResolvedPart, ResolvedText, ResolvedVerse } from '../hours/types'; import { hourLabel } from './format'; function escapeHtml(value: string): string { return value.replace(/&/g, '&').replace(//g, '>'); } function cellFor(resolved: ResolvedText | ResolvedVerse, lang: string): string { const status = resolved.status[lang] ?? 'missing'; const text = resolved.text[lang]; if (status === 'missing' || !text) { return '(translation pending)'; } const escaped = escapeHtml(text); return status === 'draft' ? `${escaped}` : escaped; } function renderColumns(resolved: ResolvedText | ResolvedVerse, languages: readonly string[]): string { const cols = languages .map((lang) => `
${cellFor(resolved, lang)}
`) .join(''); return `
${cols}
`; } // Scripture quotations always carry a full citation (book, chapter, verse) — // shown once, deduplicated across languages when they happen to match. function renderCitation(text: ResolvedText): string { if (!text.citation) { return ''; } const values = [...new Set(Object.values(text.citation).filter((v): v is string => Boolean(v)))]; return values.length ? `

${escapeHtml(values.join(' / '))}

` : ''; } function renderPart(part: ResolvedPart, languages: readonly string[]): string { switch (part.kind) { case 'hymn': case 'chapter': case 'prayer': return `

${hourLabel(part.kind)}

${renderCitation(part.text)} ${renderColumns(part.text, languages)}
`; // Short liturgical dialogue — reads fine without a jargon label. case 'responsory': case 'versicle': return `
${renderColumns(part.text, languages)}
`; case 'antiphon': return `
${renderColumns(part.text, languages)}
`; case 'preces': return `
${part.label ? `

${escapeHtml(part.label)}

` : ''} ${renderColumns(part.text, languages)}
`; case 'lesson': return `

${part.isGospel ? 'Gospel' : part.label ? escapeHtml(part.label) : 'Reading'}

${renderCitation(part.text)} ${renderColumns(part.text, languages)} ${part.responsory ? `
${renderColumns(part.responsory, languages)}
` : ''}
`; 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. const first = part.verses[0]?.n; const last = part.verses[part.verses.length - 1]?.n; const citation = first === undefined ? `Psalm ${part.psalmNumber}` : first === last ? `Psalm ${part.psalmNumber}:${first}` : `Psalm ${part.psalmNumber}:${first}-${last}`; return `
${part.antiphon ? renderColumns(part.antiphon, languages) : ''}

${citation}

    ${part.verses.map((v) => `
  1. ${renderColumns(v, languages)}
  2. `).join('')}
${part.gloriaPatri ? `
${renderColumns(part.gloriaPatri, languages)}
` : ''}
`; } case 'canticle': { const title = part.canticleId .split('-') .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); return `
${part.antiphon ? renderColumns(part.antiphon, languages) : ''}

${escapeHtml(title)}

${renderCitation(part.text)} ${renderColumns(part.text, languages)} ${part.gloriaPatri ? `
${renderColumns(part.gloriaPatri, languages)}
` : ''}
`; } // Matins only. Not currently emitted by hours/matins.ts (its psalms // render as a flat sequence of plain 'psalm'/'antiphon'/'versicle' // parts instead) — handled here for the ResolvedPart union's sake, // ready for whenever nocturn-grouped UI treatment is wanted. case 'nocturn-psalmody': return `

Nocturn ${part.nocturn}

${part.psalms.map((p) => renderPart(p, languages)).join('')}
`; case 'section-heading': return `

${escapeHtml(part.label)}

`; case 'te-deum': return `

Te Deum

${renderColumns(part.text, languages)}
`; } } export function renderHourView(container: HTMLElement): void { const { date, selectedHour, languages } = getState(); const ordo = resolveOrdo(selectedHour, date); if (ordo.notImplemented) { container.innerHTML = `

${hourLabel(selectedHour)}

This hour hasn't been built yet.

`; return; } container.innerHTML = `

${hourLabel(selectedHour)}

${ordo.parts.map((part) => renderPart(part, languages)).join('')}
`; }