added Vespers and Divine Liturgy
This commit is contained in:
1 parent
d57d8f4bef
commit
8e6c63c003
2 files changed
+231
-12
No files matched your search
@@ -22,6 +22,7 @@ export interface ServiceContext {
|
|||||||
lang: string;
|
lang: string;
|
||||||
fetchText: (path: string) => Promise<string | null>;
|
fetchText: (path: string) => Promise<string | null>;
|
||||||
fetchServiceNodes: (path: string) => Promise<ServiceNode[] | null>;
|
fetchServiceNodes: (path: string) => Promise<ServiceNode[] | null>;
|
||||||
|
fetchPrayerNodes: (path: string) => Promise<ServiceNode[] | null>;
|
||||||
fetchBibleText: (book: string, passage: string, showVerseNumbers?: boolean, verseNewLine?: boolean) => Promise<string>;
|
fetchBibleText: (book: string, passage: string, showVerseNumbers?: boolean, verseNewLine?: boolean) => Promise<string>;
|
||||||
fetchLives: (id: string) => Promise<Record<string, Record<string, Record<string, unknown>>> | null>;
|
fetchLives: (id: string) => Promise<Record<string, Record<string, Record<string, unknown>>> | null>;
|
||||||
fetchCommandText: (name: string) => Promise<string>;
|
fetchCommandText: (name: string) => Promise<string>;
|
||||||
@@ -302,14 +303,6 @@ export async function assembleService(
|
|||||||
const who = whoRaw !== undefined ? localizeWho(whoRaw, ctx) : '';
|
const who = whoRaw !== undefined ? localizeWho(whoRaw, ctx) : '';
|
||||||
let text = '';
|
let text = '';
|
||||||
let header = '';
|
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 commandName = str(n.command);
|
||||||
const commandBName = str(n.commandB);
|
const commandBName = str(n.commandB);
|
||||||
const timesVal = num(n.times);
|
const timesVal = num(n.times);
|
||||||
@@ -318,6 +311,61 @@ export async function assembleService(
|
|||||||
commandBName ? ctx.fetchCommandText(commandBName) : Promise.resolve(''),
|
commandBName ? ctx.fetchCommandText(commandBName) : Promise.resolve(''),
|
||||||
timesVal ? ctx.resolveTimes(timesVal) : 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 = {
|
const opts = {
|
||||||
who,
|
who,
|
||||||
commandText: commandText || undefined,
|
commandText: commandText || undefined,
|
||||||
|
|||||||
+175
-4
@@ -154,6 +154,20 @@ export class ServiceView {
|
|||||||
titleNode: title('Ninth1', this.t.services.serviceNames.ninth),
|
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) {
|
if (dayInfo.dRank >= 6) {
|
||||||
services.push({
|
services.push({
|
||||||
id: 'royalhours', name: this.t.services.serviceNames.royalhours, serviceType: 'ROYALHOURS',
|
id: 'royalhours', name: this.t.services.serviceNames.royalhours, serviceType: 'ROYALHOURS',
|
||||||
@@ -166,7 +180,7 @@ export class ServiceView {
|
|||||||
return services;
|
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 computed = computeDay(this.currentDate);
|
||||||
const { dayInfo } = computed;
|
const { dayInfo } = computed;
|
||||||
const tone = dayInfo.Tone;
|
const tone = dayInfo.Tone;
|
||||||
@@ -175,7 +189,7 @@ export class ServiceView {
|
|||||||
const dd = String(dayInfo.day).padStart(2, '0');
|
const dd = String(dayInfo.day).padStart(2, '0');
|
||||||
const dateKey = `${mm}-${dd}`;
|
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) {
|
if (tone > 0 && tone <= 8) {
|
||||||
const toneNum = tone === 8 ? 0 : tone;
|
const toneNum = tone === 8 ? 0 : tone;
|
||||||
@@ -193,6 +207,9 @@ export class ServiceView {
|
|||||||
return {
|
return {
|
||||||
troparion1: node.troparion1 as string | undefined,
|
troparion1: node.troparion1 as string | undefined,
|
||||||
kontakion1: node.kontakion1 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;
|
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 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] ?? '';
|
const p = prefix[serviceType] ?? '';
|
||||||
|
|
||||||
if (toneData?.troparion1) {
|
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;
|
return varNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +398,62 @@ export class ServiceView {
|
|||||||
const toneData = await this.loadToneData(service.serviceType);
|
const toneData = await this.loadToneData(service.serviceType);
|
||||||
const varNodes = this.generateVarNodes(toneData, 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 nodes: ServiceNode[] = [...templateNodes];
|
||||||
|
|
||||||
const computed = computeDay(this.currentDate);
|
const computed = computeDay(this.currentDate);
|
||||||
@@ -367,6 +476,7 @@ export class ServiceView {
|
|||||||
fetchCommandText: (name) => this.fetchCommandText(name),
|
fetchCommandText: (name) => this.fetchCommandText(name),
|
||||||
resolveTimes: (times) => this.resolveTimes(times),
|
resolveTimes: (times) => this.resolveTimes(times),
|
||||||
fetchText: this.createFetchText(),
|
fetchText: this.createFetchText(),
|
||||||
|
fetchPrayerNodes: this.createFetchPrayerNodes(),
|
||||||
fetchServiceNodes: async (path: string): Promise<ServiceNode[] | null> => {
|
fetchServiceNodes: async (path: string): Promise<ServiceNode[] | null> => {
|
||||||
const varMatch = path.match(/Services\/Var\/(\w+)\.xml$/);
|
const varMatch = path.match(/Services\/Var\/(\w+)\.xml$/);
|
||||||
if (varMatch) {
|
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> {
|
private async fetchServiceNodes(path: string): Promise<ServiceNode[] | null> {
|
||||||
const resolvePath = (lang: string): string | null => {
|
const resolvePath = (lang: string): string | null => {
|
||||||
let fetchPath = path;
|
let fetchPath = path;
|
||||||
|
|||||||
Reference in new issue
Block a user