abandoned conversion from original Java Ponomar app, migrated to own text storage format
This commit is contained in:
1 parent
edad73662c
commit
07ff6b5890
5 files changed
+119
-39
No files matched your search
+7
-2
@@ -4,10 +4,15 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run convert && vite dev",
|
"dev": "vite dev",
|
||||||
"build": "npm run convert && vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"convert": "tsx scripts/convert-all.ts",
|
"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"
|
"check": "svelte-kit sync 2>/dev/null; tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export class BibleView {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="bible-text" class="flex-1 overflow-auto p-2 max-w-3xl mx-auto liturgical-text">
|
<div id="bible-text" class="flex-1 overflow-auto p-2 max-w-3xl lg:max-w-4xl xl:max-w-5xl mx-auto liturgical-text">
|
||||||
<p class="text-navy-light italic">${this.t.bible.selectBook}</p>
|
<p class="text-navy-light italic">${this.t.bible.selectBook}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,7 +267,7 @@ export class BibleView {
|
|||||||
const isChurchSlavonic = version?.language === 'cu';
|
const isChurchSlavonic = version?.language === 'cu';
|
||||||
const fnClass = isChurchSlavonic ? fontClass(settings.cuFont) : '';
|
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 || `<p class="text-navy-light">${this.t.bible.noTextFound}</p>`;
|
textEl.innerHTML = html || `<p class="text-navy-light">${this.t.bible.noTextFound}</p>`;
|
||||||
} catch {
|
} catch {
|
||||||
textEl.innerHTML = `
|
textEl.innerHTML = `
|
||||||
|
|||||||
+47
-26
@@ -18,6 +18,8 @@ let sharedScriptureIndex: Record<string, unknown[]> | null = null;
|
|||||||
let bookAbbrevMap: Record<string, string> | null = null;
|
let bookAbbrevMap: Record<string, string> | null = null;
|
||||||
let bookCVSep = ':';
|
let bookCVSep = ':';
|
||||||
let bookAbbrevLang = '';
|
let bookAbbrevLang = '';
|
||||||
|
let cachedTriodion: Record<string, unknown> | null = null;
|
||||||
|
let cachedPentecostarion: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
function resolveDefaultBibleVersion(): string {
|
function resolveDefaultBibleVersion(): string {
|
||||||
try {
|
try {
|
||||||
@@ -130,8 +132,7 @@ interface LifeData {
|
|||||||
name?: {
|
name?: {
|
||||||
nominative?: string;
|
nominative?: string;
|
||||||
short?: string;
|
short?: string;
|
||||||
'@_Nominative'?: string;
|
shortF?: string;
|
||||||
'@_Short'?: string;
|
|
||||||
};
|
};
|
||||||
life?: {
|
life?: {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -307,11 +308,11 @@ async function loadCommemoration(sharedId: string): Promise<CommemorationData |
|
|||||||
|
|
||||||
function getPeriodName(life: LifeData | undefined): string {
|
function getPeriodName(life: LifeData | undefined): string {
|
||||||
if (!life?.name) return '';
|
if (!life?.name) return '';
|
||||||
return life.name['@_Nominative'] || life.name.nominative || life.name['@_Short'] || life.name.short || '';
|
return life.name.nominative || life.name.short || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShortFeastName(life: LifeData | undefined, shared: CommemorationData | null | undefined): string {
|
function getShortFeastName(life: LifeData | undefined, shared: CommemorationData | null | undefined): string {
|
||||||
if (life?.name?.['@_Short']) return life.name['@_Short'];
|
if (life?.name?.short) return life.name.short;
|
||||||
if (life?.name?.short) return life.name.short;
|
if (life?.name?.short) return life.name.short;
|
||||||
return getCommsName(shared);
|
return getCommsName(shared);
|
||||||
}
|
}
|
||||||
@@ -347,24 +348,46 @@ async function loadMenaion(lang: LanguageCode, month: number, day: number): Prom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTriodionFile(index: number): Promise<unknown[]> {
|
async function loadTriodionData(): Promise<Record<string, unknown>> {
|
||||||
|
if (cachedTriodion) return cachedTriodion;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/data/shared/triodion/${String(index).padStart(2, '0')}.json`);
|
const resp = await fetch('/data/shared/calendar/triodion.json');
|
||||||
if (!resp.ok) return [];
|
if (!resp.ok) return {};
|
||||||
return await resp.json();
|
const ct = resp.headers.get('content-type');
|
||||||
|
if (ct && ct.includes('text/html')) return {};
|
||||||
|
cachedTriodion = await resp.json();
|
||||||
|
return cachedTriodion!;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTriodionFile(index: number): Promise<unknown[]> {
|
||||||
|
const data = await loadTriodionData();
|
||||||
|
const key = String(index).padStart(2, '0');
|
||||||
|
const entry = data[key];
|
||||||
|
return entry ? [entry] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPentecostarionData(): Promise<Record<string, unknown>> {
|
||||||
|
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<unknown[]> {
|
async function loadPentecostarionFile(index: number): Promise<unknown[]> {
|
||||||
try {
|
const data = await loadPentecostarionData();
|
||||||
const resp = await fetch(`/data/shared/pentecostarion/${String(index).padStart(2, '0')}.json`);
|
const key = String(index).padStart(2, '0');
|
||||||
if (!resp.ok) return [];
|
const entry = data[key];
|
||||||
return await resp.json();
|
return entry ? [entry] : [];
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CalendarView {
|
export class CalendarView {
|
||||||
@@ -544,7 +567,7 @@ export class CalendarView {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 p-6 overflow-auto">
|
||||||
<div class="max-w-2xl">
|
<div class="max-w-2xl lg:max-w-3xl xl:max-w-4xl">
|
||||||
<h2 class="text-base font-bold text-navy mb-1">
|
<h2 class="text-base font-bold text-navy mb-1">
|
||||||
${julianStr}
|
${julianStr}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -653,7 +676,7 @@ export class CalendarView {
|
|||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
if (typeof entry === 'object' && entry !== null) {
|
if (typeof entry === 'object' && entry !== null) {
|
||||||
const e = entry as Record<string, unknown>;
|
const e = entry as Record<string, unknown>;
|
||||||
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 feastName = '';
|
||||||
let feastRank = 0;
|
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) {
|
for (const entry of calendarEntries) {
|
||||||
const e = entry as Record<string, unknown>;
|
const e = entry as Record<string, unknown>;
|
||||||
const cid = String(e.CId || '');
|
const cid = String(e.id || '');
|
||||||
const sid = String(e.SId ?? '');
|
|
||||||
if (!cid || !/^9[0-38]\d{2}$/.test(cid)) continue;
|
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));
|
const name = getPeriodName(livesData.get(cid)) || getCommsName(sharedComms.get(cid));
|
||||||
if (!name) continue;
|
if (!name) continue;
|
||||||
periodName = name;
|
periodName = name;
|
||||||
@@ -699,7 +720,7 @@ export class CalendarView {
|
|||||||
// Pass 2: Find great feast (rank >= 6) from all entries
|
// Pass 2: Find great feast (rank >= 6) from all entries
|
||||||
for (const entry of allEntries) {
|
for (const entry of allEntries) {
|
||||||
const e = entry as Record<string, unknown>;
|
const e = entry as Record<string, unknown>;
|
||||||
const cid = String(e.CId || '');
|
const cid = String(e.id || '');
|
||||||
if (!cid || this.skipCids.has(cid)) continue;
|
if (!cid || this.skipCids.has(cid)) continue;
|
||||||
const life = livesData.get(cid);
|
const life = livesData.get(cid);
|
||||||
const shared = sharedComms.get(cid);
|
const shared = sharedComms.get(cid);
|
||||||
@@ -749,7 +770,7 @@ export class CalendarView {
|
|||||||
|
|
||||||
let name = '';
|
let name = '';
|
||||||
if (life?.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 (!name && shared?.NAME) {
|
||||||
if (typeof shared.NAME === 'object' && 'Long' in shared.NAME) {
|
if (typeof shared.NAME === 'object' && 'Long' in shared.NAME) {
|
||||||
@@ -811,7 +832,7 @@ export class CalendarView {
|
|||||||
if (cid && isPeriodCid(cid)) return '';
|
if (cid && isPeriodCid(cid)) return '';
|
||||||
return ` (${shortName})`;
|
return ` (${shortName})`;
|
||||||
};
|
};
|
||||||
loadBookAbbrevs(this.language);
|
await loadBookAbbrevs(this.language);
|
||||||
for (const [cid, shared] of sharedComms) {
|
for (const [cid, shared] of sharedComms) {
|
||||||
if (shared?.SCRIPTURE) {
|
if (shared?.SCRIPTURE) {
|
||||||
const shortN = typeof shared.NAME === 'object' && 'ShortN' in shared.NAME ? (shared.NAME.ShortN || '') : '';
|
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)
|
// Also load readings from saint lives (from services structure)
|
||||||
for (const [cid, life] of livesData) {
|
for (const [cid, life] of livesData) {
|
||||||
const shortName = life?.name?.['@_Short'] || life?.name?.short || '';
|
const shortName = life?.name?.short || '';
|
||||||
const tag = readingContext(shortName, cid);
|
const tag = readingContext(shortName, cid);
|
||||||
|
|
||||||
if (life?.services) {
|
if (life?.services) {
|
||||||
|
|||||||
+62
-8
@@ -88,7 +88,7 @@ export class ServiceView {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
this.container.innerHTML = standalone
|
this.container.innerHTML = standalone
|
||||||
? `<div class="p-6 max-w-4xl mx-auto">
|
? `<div class="p-6 max-w-4xl xl:max-w-6xl mx-auto">
|
||||||
<h2 class="text-2xl font-bold text-navy mb-2">${this.t.services.title}</h2>
|
<h2 class="text-2xl font-bold text-navy mb-2">${this.t.services.title}</h2>
|
||||||
<p class="text-navy-light mb-4">
|
<p class="text-navy-light mb-4">
|
||||||
${this.currentDate.toString()}
|
${this.currentDate.toString()}
|
||||||
@@ -183,7 +183,7 @@ export class ServiceView {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const tryFetch = async (lang: string) => {
|
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);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) return null;
|
if (!resp.ok) return null;
|
||||||
const nodes: ServiceNode[] = await resp.json();
|
const nodes: ServiceNode[] = await resp.json();
|
||||||
@@ -214,9 +214,9 @@ export class ServiceView {
|
|||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) return null;
|
if (!resp.ok) return null;
|
||||||
const all = await resp.json();
|
const all = await resp.json();
|
||||||
if (Array.isArray(all)) return all.map((e: Record<string, unknown>) => ({ cid: String(e.CId ?? '') }));
|
if (Array.isArray(all)) return all.map((e: Record<string, unknown>) => ({ cid: String(e.id ?? '') }));
|
||||||
const entries = all[dateKey] as Record<string, unknown>[] | undefined;
|
const entries = all[dateKey] as Record<string, unknown>[] | undefined;
|
||||||
if (Array.isArray(entries)) return entries.map((e: Record<string, unknown>) => ({ cid: String(e.CId ?? '') }));
|
if (Array.isArray(entries)) return entries.map((e: Record<string, unknown>) => ({ cid: String(e.id ?? '') }));
|
||||||
return null;
|
return null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
};
|
};
|
||||||
@@ -226,12 +226,24 @@ export class ServiceView {
|
|||||||
const menaionEntries = await tryLoad(`/data/${this.serviceLang}/menaion-bundle.json`);
|
const menaionEntries = await tryLoad(`/data/${this.serviceLang}/menaion-bundle.json`);
|
||||||
if (menaionEntries) allEntries.push(...menaionEntries);
|
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) {
|
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 (triodion) allEntries.push(...triodion);
|
||||||
}
|
}
|
||||||
if (dayInfo.pentecostarionFile !== null) {
|
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);
|
if (pent) allEntries.push(...pent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,7 +620,28 @@ export class ServiceView {
|
|||||||
const resolvePath = (lang: string): string | null => {
|
const resolvePath = (lang: string): string | null => {
|
||||||
let fetchPath = basePath;
|
let fetchPath = basePath;
|
||||||
if (fetchPath.startsWith('Services/')) {
|
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}`;
|
return `/data/${lang}/services/${fetchPath}`;
|
||||||
};
|
};
|
||||||
@@ -673,7 +706,28 @@ export class ServiceView {
|
|||||||
const resolvePath = (lang: string): string | null => {
|
const resolvePath = (lang: string): string | null => {
|
||||||
let fetchPath = path;
|
let fetchPath = path;
|
||||||
if (fetchPath.startsWith('Services/')) {
|
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}`;
|
return `/data/${lang}/services/${fetchPath}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export class SettingsView {
|
|||||||
const t = getTranslations(this.settings.language as LanguageCode);
|
const t = getTranslations(this.settings.language as LanguageCode);
|
||||||
|
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
<div class="p-6 max-w-2xl mx-auto">
|
<div class="p-6 max-w-2xl lg:max-w-3xl xl:max-w-4xl mx-auto">
|
||||||
<h2 class="text-2xl font-bold text-navy mb-6">${t.settings.title}</h2>
|
<h2 class="text-2xl font-bold text-navy mb-6">${t.settings.title}</h2>
|
||||||
|
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
|
|||||||
Reference in new issue
Block a user