diff --git a/package.json b/package.json index 4f6a573..72ba4b1 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,15 @@ "version": "0.1.0", "type": "module", "scripts": { - "dev": "npm run convert && vite dev", - "build": "npm run convert && vite build", + "dev": "vite dev", + "build": "vite build", "preview": "vite preview", "convert": "tsx scripts/convert-all.ts", + "migrate": "tsx scripts/migrate-data.ts", + "migrate:dry": "tsx scripts/migrate-data.ts --dry-run", + "validate": "tsx scripts/validate-data.ts", + "search": "tsx scripts/tools/search-texts.ts", + "stats": "tsx scripts/tools/stats.ts", "check": "svelte-kit sync 2>/dev/null; tsc --noEmit" }, "devDependencies": { diff --git a/src/ui/bible-view.ts b/src/ui/bible-view.ts index e027467..a2c23e6 100644 --- a/src/ui/bible-view.ts +++ b/src/ui/bible-view.ts @@ -133,7 +133,7 @@ export class BibleView { -
+

${this.t.bible.selectBook}

@@ -267,7 +267,7 @@ export class BibleView { const isChurchSlavonic = version?.language === 'cu'; const fnClass = isChurchSlavonic ? fontClass(settings.cuFont) : ''; - textEl.className = `flex-1 overflow-auto p-2 max-w-3xl mx-auto liturgical-text ${fnClass}`; + textEl.className = `flex-1 overflow-auto p-2 max-w-3xl lg:max-w-4xl xl:max-w-5xl mx-auto liturgical-text ${fnClass}`; textEl.innerHTML = html || `

${this.t.bible.noTextFound}

`; } catch { textEl.innerHTML = ` diff --git a/src/ui/calendar-view.ts b/src/ui/calendar-view.ts index cdde05e..6ad8a3d 100644 --- a/src/ui/calendar-view.ts +++ b/src/ui/calendar-view.ts @@ -18,6 +18,8 @@ let sharedScriptureIndex: Record | null = null; let bookAbbrevMap: Record | null = null; let bookCVSep = ':'; let bookAbbrevLang = ''; +let cachedTriodion: Record | null = null; +let cachedPentecostarion: Record | null = null; function resolveDefaultBibleVersion(): string { try { @@ -130,8 +132,7 @@ interface LifeData { name?: { nominative?: string; short?: string; - '@_Nominative'?: string; - '@_Short'?: string; + shortF?: string; }; life?: { text: string; @@ -307,11 +308,11 @@ async function loadCommemoration(sharedId: string): Promise { +async function loadTriodionData(): Promise> { + if (cachedTriodion) return cachedTriodion; try { - const resp = await fetch(`/data/shared/triodion/${String(index).padStart(2, '0')}.json`); - if (!resp.ok) return []; - return await resp.json(); + const resp = await fetch('/data/shared/calendar/triodion.json'); + if (!resp.ok) return {}; + const ct = resp.headers.get('content-type'); + if (ct && ct.includes('text/html')) return {}; + cachedTriodion = await resp.json(); + return cachedTriodion!; } catch { - return []; + return {}; + } +} + +async function loadTriodionFile(index: number): Promise { + const data = await loadTriodionData(); + const key = String(index).padStart(2, '0'); + const entry = data[key]; + return entry ? [entry] : []; +} + +async function loadPentecostarionData(): Promise> { + if (cachedPentecostarion) return cachedPentecostarion; + try { + const resp = await fetch('/data/shared/calendar/pentecostarion.json'); + if (!resp.ok) return {}; + const ct = resp.headers.get('content-type'); + if (ct && ct.includes('text/html')) return {}; + cachedPentecostarion = await resp.json(); + return cachedPentecostarion!; + } catch { + return {}; } } async function loadPentecostarionFile(index: number): Promise { - try { - const resp = await fetch(`/data/shared/pentecostarion/${String(index).padStart(2, '0')}.json`); - if (!resp.ok) return []; - return await resp.json(); - } catch { - return []; - } + const data = await loadPentecostarionData(); + const key = String(index).padStart(2, '0'); + const entry = data[key]; + return entry ? [entry] : []; } export class CalendarView { @@ -544,7 +567,7 @@ export class CalendarView {
-
+

${julianStr}

@@ -653,7 +676,7 @@ export class CalendarView { for (const entry of allEntries) { if (typeof entry === 'object' && entry !== null) { const e = entry as Record; - if (e.CId) cids.push(String(e.CId)); + if (e.id) cids.push(String(e.id)); } } @@ -682,13 +705,11 @@ export class CalendarView { let feastName = ''; let feastRank = 0; - // Pass 1: Find period name from primary calendar entry (SId ''/'0' + CId 9[0-38]xxxx) + // Pass 1: Find period name from primary calendar entry (id 9[0-38]xxxx) for (const entry of calendarEntries) { const e = entry as Record; - const cid = String(e.CId || ''); - const sid = String(e.SId ?? ''); + const cid = String(e.id || ''); if (!cid || !/^9[0-38]\d{2}$/.test(cid)) continue; - if (sid !== '' && sid !== '0') continue; const name = getPeriodName(livesData.get(cid)) || getCommsName(sharedComms.get(cid)); if (!name) continue; periodName = name; @@ -699,7 +720,7 @@ export class CalendarView { // Pass 2: Find great feast (rank >= 6) from all entries for (const entry of allEntries) { const e = entry as Record; - const cid = String(e.CId || ''); + const cid = String(e.id || ''); if (!cid || this.skipCids.has(cid)) continue; const life = livesData.get(cid); const shared = sharedComms.get(cid); @@ -749,7 +770,7 @@ export class CalendarView { let name = ''; if (life?.name) { - name = life.name['@_Nominative'] || life.name.nominative || life.name['@_Short'] || life.name.short || ''; + name = life.name.nominative || life.name.short || ''; } if (!name && shared?.NAME) { if (typeof shared.NAME === 'object' && 'Long' in shared.NAME) { @@ -811,7 +832,7 @@ export class CalendarView { if (cid && isPeriodCid(cid)) return ''; return ` (${shortName})`; }; - loadBookAbbrevs(this.language); + await loadBookAbbrevs(this.language); for (const [cid, shared] of sharedComms) { if (shared?.SCRIPTURE) { const shortN = typeof shared.NAME === 'object' && 'ShortN' in shared.NAME ? (shared.NAME.ShortN || '') : ''; @@ -830,7 +851,7 @@ export class CalendarView { } // Also load readings from saint lives (from services structure) for (const [cid, life] of livesData) { - const shortName = life?.name?.['@_Short'] || life?.name?.short || ''; + const shortName = life?.name?.short || ''; const tag = readingContext(shortName, cid); if (life?.services) { diff --git a/src/ui/service-view.ts b/src/ui/service-view.ts index 821afa7..91c831d 100644 --- a/src/ui/service-view.ts +++ b/src/ui/service-view.ts @@ -88,7 +88,7 @@ export class ServiceView { `; this.container.innerHTML = standalone - ? `
+ ? `

${this.t.services.title}

${this.currentDate.toString()} @@ -183,7 +183,7 @@ export class ServiceView { try { const tryFetch = async (lang: string) => { - const url = `/data/${lang}/services/Octoecheos/Tone ${toneNum}/${weekday}.json`; + const url = `/data/${lang}/services/octoecheos/tone${toneNum}/${weekday.toLowerCase()}.json`; const resp = await fetch(url); if (!resp.ok) return null; const nodes: ServiceNode[] = await resp.json(); @@ -214,9 +214,9 @@ export class ServiceView { const resp = await fetch(url); if (!resp.ok) return null; const all = await resp.json(); - if (Array.isArray(all)) return all.map((e: Record) => ({ cid: String(e.CId ?? '') })); + if (Array.isArray(all)) return all.map((e: Record) => ({ cid: String(e.id ?? '') })); const entries = all[dateKey] as Record[] | undefined; - if (Array.isArray(entries)) return entries.map((e: Record) => ({ cid: String(e.CId ?? '') })); + if (Array.isArray(entries)) return entries.map((e: Record) => ({ cid: String(e.id ?? '') })); return null; } catch { return null; } }; @@ -226,12 +226,24 @@ export class ServiceView { const menaionEntries = await tryLoad(`/data/${this.serviceLang}/menaion-bundle.json`); if (menaionEntries) allEntries.push(...menaionEntries); + // Load from merged calendar files + const loadCalendarEntry = async (file: string, index: number): Promise<{ cid: string }[] | null> => { + try { + const resp = await fetch(`/data/shared/calendar/${file}.json`); + if (!resp.ok) return null; + const data = await resp.json(); + const key = String(index).padStart(2, '0'); + const entry = data[key]; + return entry ? [{ cid: String(entry.id ?? '') }] : null; + } catch { return null; } + }; + if (dayInfo.triodionFile !== null) { - const triodion = await tryLoad(`/data/shared/triodion/${dayInfo.triodionFile}.json`); + const triodion = await loadCalendarEntry('triodion', dayInfo.triodionFile); if (triodion) allEntries.push(...triodion); } if (dayInfo.pentecostarionFile !== null) { - const pent = await tryLoad(`/data/shared/pentecostarion/${dayInfo.pentecostarionFile}.json`); + const pent = await loadCalendarEntry('pentecostarion', dayInfo.pentecostarionFile); if (pent) allEntries.push(...pent); } @@ -608,7 +620,28 @@ export class ServiceView { const resolvePath = (lang: string): string | null => { let fetchPath = basePath; if (fetchPath.startsWith('Services/')) { - fetchPath = fetchPath.replace('Services/', '').replace('.xml', '.json'); + fetchPath = fetchPath.slice('Services/'.length); + // Handle different path patterns + if (fetchPath.startsWith('CommonPrayers/')) { + // CommonPrayers -> prayers + fetchPath = 'prayers/' + fetchPath.slice('CommonPrayers/'.length); + } else if (fetchPath.startsWith('Text/')) { + // Text -> texts + fetchPath = 'texts/' + fetchPath.slice('Text/'.length); + } else if (fetchPath.startsWith('Command/')) { + // Command -> commands + fetchPath = 'commands/' + fetchPath.slice('Command/'.length); + } else if (fetchPath.startsWith('Header/')) { + // Header -> headers + fetchPath = 'headers/' + fetchPath.slice('Header/'.length); + } else if (fetchPath.startsWith('Var/')) { + // Var -> var (lowercase) + fetchPath = 'var/' + fetchPath.slice('Var/'.length); + } else if (fetchPath.startsWith('Octoecheos/')) { + // Octoecheos -> octoecheos + fetchPath = 'octoecheos/' + fetchPath.slice('Octoecheos/'.length); + } + fetchPath = fetchPath.replace('.xml', '.json'); } return `/data/${lang}/services/${fetchPath}`; }; @@ -673,7 +706,28 @@ export class ServiceView { const resolvePath = (lang: string): string | null => { let fetchPath = path; if (fetchPath.startsWith('Services/')) { - fetchPath = fetchPath.replace('Services/', '').replace('.xml', '.json'); + fetchPath = fetchPath.slice('Services/'.length); + // Service templates are in templates/ subdirectory + // But if path contains a subdirectory, don't add templates/ prefix + if (!fetchPath.includes('/')) { + fetchPath = 'templates/' + fetchPath; + } else { + // Handle subdirectories like Var/, CommonPrayers/, etc. + if (fetchPath.startsWith('CommonPrayers/')) { + fetchPath = 'prayers/' + fetchPath.slice('CommonPrayers/'.length); + } else if (fetchPath.startsWith('Text/')) { + fetchPath = 'texts/' + fetchPath.slice('Text/'.length); + } else if (fetchPath.startsWith('Command/')) { + fetchPath = 'commands/' + fetchPath.slice('Command/'.length); + } else if (fetchPath.startsWith('Header/')) { + fetchPath = 'headers/' + fetchPath.slice('Header/'.length); + } else if (fetchPath.startsWith('Var/')) { + fetchPath = 'var/' + fetchPath.slice('Var/'.length); + } else if (fetchPath.startsWith('Octoecheos/')) { + fetchPath = 'octoecheos/' + fetchPath.slice('Octoecheos/'.length); + } + } + fetchPath = fetchPath.replace('.xml', '.json'); } return `/data/${lang}/services/${fetchPath}`; }; diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index edf6f31..c6dd2be 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -111,7 +111,7 @@ export class SettingsView { const t = getTranslations(this.settings.language as LanguageCode); this.container.innerHTML = ` -

+

${t.settings.title}