added Vespers and Divine Liturgy

This commit is contained in:
catarrh committed 2026-07-30 00:33:38 +03:00
1 parent d57d8f4bef
commit 8e6c63c003
2 files changed
+231 -12

No files matched your search

+56 -8
View File
@@ -22,6 +22,7 @@ export interface ServiceContext {
lang: string;
fetchText: (path: string) => Promise<string | null>;
fetchServiceNodes: (path: string) => Promise<ServiceNode[] | null>;
fetchPrayerNodes: (path: string) => Promise<ServiceNode[] | null>;
fetchBibleText: (book: string, passage: string, showVerseNumbers?: boolean, verseNewLine?: boolean) => Promise<string>;
fetchLives: (id: string) => Promise<Record<string, Record<string, Record<string, unknown>>> | null>;
fetchCommandText: (name: string) => Promise<string>;
@@ -302,14 +303,6 @@ export async function assembleService(
const who = whoRaw !== undefined ? localizeWho(whoRaw, ctx) : '';
let text = '';
let header = '';
if (what) {
text = await ctx.fetchText(`Services/CommonPrayers/${what}.xml`) ?? '';
// Fallback: if fetchText returns empty, try to use the 'what' field as a key
if (!text.trim()) {
text = what; // fallback to the key itself
}
header = await ctx.fetchText(`Services/CommonPrayers/${what}.xml.header`) ?? '';
}
const commandName = str(n.command);
const commandBName = str(n.commandB);
const timesVal = num(n.times);
@@ -318,6 +311,61 @@ export async function assembleService(
commandBName ? ctx.fetchCommandText(commandBName) : Promise.resolve(''),
timesVal ? ctx.resolveTimes(timesVal) : Promise.resolve(''),
]);
if (what) {
// Try fetching structured prayer nodes (preserves per-line who)
const prayerNodes = await ctx.fetchPrayerNodes(`Services/CommonPrayers/${what}.xml`);
if (prayerNodes && prayerNodes.length > 0) {
header = await ctx.fetchText(`Services/CommonPrayers/${what}.xml.header`) ?? '';
const headerHtml = n.header && header
? `<h2 class="text-center text-red font-bold mt-4">${header}</h2>`
: '';
if (headerHtml) html += headerHtml + '\n';
// Check if any TEXT nodes have their own who field
const hasWhoNodes = prayerNodes.some(n => n.type === 'TEXT' && str((n as Record<string, unknown>).who) !== undefined);
if (hasWhoNodes) {
for (const pn of prayerNodes) {
if (pn.type !== 'TEXT') continue;
const pnWhoRaw = str((pn as Record<string, unknown>).who);
const pnWho = pnWhoRaw !== undefined ? localizeWho(pnWhoRaw, ctx) : '';
const pnText = str((pn as Record<string, unknown>).what) ?? str((pn as Record<string, unknown>).value) ?? '';
const pnOpts = {
who: pnWho,
commandText: commandText || undefined,
commandBText: commandBText || undefined,
timesText: timesText || undefined,
redFirst: bool((pn as Record<string, unknown>).redFirst) ?? bool(n.redFirst),
newLine: bool((pn as Record<string, unknown>).newLine) ?? bool(n.newLine),
headerLevel: 0,
};
const pnFmt = formatServiceContent(pnText, pnOpts);
if (pnWho) {
if (pnWho !== groupWho) flushGroup();
groupWho = pnWho;
groupTexts.push(pnFmt);
} else {
flushGroup();
const br = pnOpts.newLine ? '<br>' : '';
html += `${br}<p>${pnFmt}</p>`;
}
}
if (whoRaw !== undefined) whoLast = who;
break;
}
// No who nodes — fall back to flattening like fetchText
text = prayerNodes
.filter(n => n.type === 'TEXT')
.map(n => String(str((n as Record<string, unknown>).value) ?? '').replace(/\n/g, ' '))
.filter(Boolean)
.join('<br>') || '';
} else {
// No structured nodes — use fetchText
text = await ctx.fetchText(`Services/CommonPrayers/${what}.xml`) ?? '';
if (!text.trim()) {
text = what;
}
header = await ctx.fetchText(`Services/CommonPrayers/${what}.xml.header`) ?? '';
}
}
const opts = {
who,
commandText: commandText || undefined,
+175 -4
View File
@@ -154,6 +154,20 @@ export class ServiceView {
titleNode: title('Ninth1', this.t.services.serviceNames.ninth),
});
services.push({
id: 'vespers', name: this.t.services.serviceNames.vespers, serviceType: 'VESPERS',
template: 'Vespers',
description: this.t.services.serviceDescriptions.vespers,
titleNode: title('Vespers1', this.t.services.serviceNames.vespers, 'VesperSource'),
});
services.push({
id: 'liturgy', name: this.t.services.serviceNames.liturgy, serviceType: 'LITURGY',
template: 'DivineLiturgy',
description: this.t.services.serviceDescriptions.liturgy,
titleNode: title('DivineLiturgyTitle', this.t.services.serviceNames.liturgy, 'DivineLiturgySource'),
});
if (dayInfo.dRank >= 6) {
services.push({
id: 'royalhours', name: this.t.services.serviceNames.royalhours, serviceType: 'ROYALHOURS',
@@ -166,7 +180,7 @@ export class ServiceView {
return services;
}
private async loadToneData(serviceType: string): Promise<{ troparion1?: string; kontakion1?: string } | null> {
private async loadToneData(serviceType: string): Promise<{ troparion1?: string; kontakion1?: string; theotokion1?: string; prokimenon1?: string; alleluia1?: string } | null> {
const computed = computeDay(this.currentDate);
const { dayInfo } = computed;
const tone = dayInfo.Tone;
@@ -175,7 +189,7 @@ export class ServiceView {
const dd = String(dayInfo.day).padStart(2, '0');
const dateKey = `${mm}-${dd}`;
let result: { troparion1?: string; kontakion1?: string } | null = null;
let result: { troparion1?: string; kontakion1?: string; theotokion1?: string; prokimenon1?: string; alleluia1?: string } | null = null;
if (tone > 0 && tone <= 8) {
const toneNum = tone === 8 ? 0 : tone;
@@ -193,6 +207,9 @@ export class ServiceView {
return {
troparion1: node.troparion1 as string | undefined,
kontakion1: node.kontakion1 as string | undefined,
theotokion1: node.theotokion1 as string | undefined,
prokimenon1: node.prokimenon1 as string | undefined,
alleluia1: node.alleluia1 as string | undefined,
};
}
}
@@ -296,10 +313,10 @@ export class ServiceView {
return result;
}
private generateVarNodes(toneData: { troparion1?: string; kontakion1?: string } | null, serviceType: string): Map<string, ServiceNode[]> {
private generateVarNodes(toneData: { troparion1?: string; kontakion1?: string; theotokion1?: string; prokimenon1?: string; alleluia1?: string } | null, serviceType: string): Map<string, ServiceNode[]> {
const varNodes = new Map<string, ServiceNode[]>();
const prefix: Record<string, string> = { PRIMES: '', TERCE: '3', SEXTE: '6', NONE: '9' };
const prefix: Record<string, string> = { PRIMES: '', TERCE: '3', SEXTE: '6', NONE: '9', VESPERS: 'V', LITURGY: 'L' };
const p = prefix[serviceType] ?? '';
if (toneData?.troparion1) {
@@ -326,6 +343,42 @@ export class ServiceView {
}]);
}
if (toneData?.theotokion1) {
const key = `PTheot${p}1`;
varNodes.set(key, [{
type: 'CREATE',
what: `THEOTOKION/${toneData.theotokion1}`,
who: '',
header: true,
redfirst: true,
newline: true,
}]);
}
if (toneData?.prokimenon1) {
const key = `PProk${p}1`;
varNodes.set(key, [{
type: 'CREATE',
what: toneData.prokimenon1,
who: '',
header: true,
redfirst: true,
newline: true,
}]);
}
if (toneData?.alleluia1) {
const key = `PAllel${p}1`;
varNodes.set(key, [{
type: 'CREATE',
what: toneData.alleluia1,
who: '',
header: true,
redfirst: true,
newline: true,
}]);
}
return varNodes;
}
@@ -345,6 +398,62 @@ export class ServiceView {
const toneData = await this.loadToneData(service.serviceType);
const varNodes = this.generateVarNodes(toneData, service.serviceType);
// Load Epistle and Gospel readings from commemoration
const loadReadings = async () => {
const ids: string[] = [];
const mm = String(this.currentDate.getMonth()).padStart(2, '0');
const dd = String(this.currentDate.getDay()).padStart(2, '0');
const dateKey = `${mm}-${dd}`;
try {
const resp = await fetch(`/data/${this.serviceLang}/menaion-bundle.json`);
if (resp.ok) {
const bundle = await resp.json();
const entries = bundle[dateKey];
if (entries) {
for (const e of entries) {
if (e.id) ids.push(String(e.id));
}
}
}
} catch {}
for (const cid of ids) {
try {
const resp = await fetch(`/data/shared/commemorations/${cid}.json`);
if (!resp.ok) continue;
const data = await resp.json();
const scripts = data.SCRIPTURE;
if (!Array.isArray(scripts)) continue;
let epistlePassage = '';
let gospelPassage = '';
for (const s of scripts) {
const r = s.Reading || '';
if (s.Type === 'apostol' && r && !epistlePassage) epistlePassage = r;
if (s.Type === 'gospel' && r && !gospelPassage) gospelPassage = r;
}
if (epistlePassage) {
varNodes.set('EpistleReading', [{
type: 'BIBLE',
verses: epistlePassage,
who: 'R',
redfirst: true,
newline: true,
}]);
}
if (gospelPassage) {
varNodes.set('GospelReading', [{
type: 'BIBLE',
verses: gospelPassage,
who: 'D',
redfirst: true,
newline: true,
}]);
}
if (epistlePassage || gospelPassage) break;
} catch {}
}
};
await loadReadings();
const nodes: ServiceNode[] = [...templateNodes];
const computed = computeDay(this.currentDate);
@@ -367,6 +476,7 @@ export class ServiceView {
fetchCommandText: (name) => this.fetchCommandText(name),
resolveTimes: (times) => this.resolveTimes(times),
fetchText: this.createFetchText(),
fetchPrayerNodes: this.createFetchPrayerNodes(),
fetchServiceNodes: async (path: string): Promise<ServiceNode[] | null> => {
const varMatch = path.match(/Services\/Var\/(\w+)\.xml$/);
if (varMatch) {
@@ -702,6 +812,67 @@ export class ServiceView {
};
}
private createFetchPrayerNodes() {
return async (path: string): Promise<ServiceNode[] | null> => {
const resolvePath = (lang: string): string | null => {
let fetchPath = path;
if (fetchPath.startsWith('Services/')) {
fetchPath = fetchPath.slice('Services/'.length);
if (fetchPath.startsWith('CommonPrayers/')) {
fetchPath = 'prayers/' + fetchPath.slice('CommonPrayers/'.length);
}
fetchPath = fetchPath.replace('.xml', '.json');
}
return `/data/${lang}/services/${fetchPath}`;
};
const tryFetch = async (url: string): Promise<ServiceNode[] | null> => {
if (this.notFound.has(url)) return null;
const cached = this.nodeCache.get(url);
if (cached) return cached;
try {
const resp = await fetch(url);
if (!resp.ok) { this.notFound.add(url); return null; }
const ct = resp.headers.get('content-type') || '';
if (ct.includes('text/html')) { this.notFound.add(url); return null; }
const nodes = await resp.json();
if (Array.isArray(nodes)) {
this.nodeCache.set(url, nodes);
return nodes;
}
this.notFound.add(url);
return null;
} catch {
this.notFound.add(url);
return null;
}
};
try {
let url = resolvePath(this.serviceLang);
if (url) {
const result = await tryFetch(url);
if (result) return result;
}
if (this.serviceLang !== 'cu') {
const fbUrl = resolvePath('cu');
if (fbUrl) {
const result = await tryFetch(fbUrl);
if (result) return result;
}
}
const sharedUrl = resolvePath('shared');
if (sharedUrl) {
return await tryFetch(sharedUrl);
}
return null;
} catch (err) {
console.error('Service fetchPrayerNodes error:', err);
return null;
}
};
}
private async fetchServiceNodes(path: string): Promise<ServiceNode[] | null> {
const resolvePath = (lang: string): string | null => {
let fetchPath = path;