added Vespers and Divine Liturgy
This commit is contained in:
1 parent
d57d8f4bef
commit
8e6c63c003
2 files changed
+234
-15
No files matched your search
@@ -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,
|
||||
|
||||
+178
-7
@@ -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) {
|
||||
@@ -699,10 +809,71 @@ export class ServiceView {
|
||||
console.error('Service fetchText error:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchServiceNodes(path: string): Promise<ServiceNode[] | null> {
|
||||
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;
|
||||
if (fetchPath.startsWith('Services/')) {
|
||||
|
||||
Reference in new issue
Block a user