UNPKG

@jager-ai/holy-editor

Version:

Rich text editor with Bible verse slash commands and PWA keyboard tracking, extracted from Holy Habit project

286 lines (247 loc) 8.61 kB
/** * Bible Verse Engine * * Core engine for Bible verse slash commands and API integration * Extracted from Holy Habit holy-editor-pro.js */ import { BibleVerse, BibleVerseData, BibleApiResponse, BibleApiError, BibleBookMapping, VerseCache, SlashCommandMatch } from '../types/Editor'; export class BibleVerseEngine { private static instance: BibleVerseEngine; private cache: VerseCache = {}; private lastInsertedRef = ''; private lastInsertedTime = 0; private debounceMs = 300; private apiEndpoint = '/api/bible_verse_full.php'; // Bible book mappings (66 books total) private static readonly OLD_TESTAMENT_BOOKS: BibleBookMapping = { 창: '창세기', 출: '출애굽기', 레: '레위기', 민: '민수기', 신: '신명기', 수: '여호수아', 삿: '사사기', 룻: '룻기', 삼상: '사무엘상', 삼하: '사무엘하', 왕상: '열왕기상', 왕하: '열왕기하', 대상: '역대상', 대하: '역대하', 스: '에스라', 느: '느헤미야', 에: '에스더', 욥: '욥기', 시: '시편', 잠: '잠언', 전: '전도서', 아: '아가', 사: '이사야', 렘: '예레미야', 애: '예레미야애가', 겔: '에스겔', 단: '다니엘', 호: '호세아', 욜: '요엘', 암: '아모스', 옵: '오바댜', 욘: '요나', 미: '미가', 나: '나훔', 합: '하박국', 습: '스바냐', 학: '학개', 슥: '스가랴', 말: '말라기' }; private static readonly NEW_TESTAMENT_BOOKS: BibleBookMapping = { 마: '마태복음', 막: '마가복음', 눅: '누가복음', 요: '요한복음', 행: '사도행전', 롬: '로마서', 고전: '고린도전서', 고후: '고린도후서', 갈: '갈라디아서', 엡: '에베소서', 빌: '빌립보서', 골: '골로새서', 살전: '데살로니가전서', 살후: '데살로니가후서', 딤전: '디모데전서', 딤후: '디모데후서', 딛: '디도서', 몬: '빌레몬서', 히: '히브리서', 약: '야고보서', 벧전: '베드로전서', 벧후: '베드로후서', 요일: '요한일서', 요이: '요한이서', 요삼: '요한삼서', 유: '유다서', 계: '요한계시록' }; private static readonly ALL_BOOKS: BibleBookMapping = { ...BibleVerseEngine.OLD_TESTAMENT_BOOKS, ...BibleVerseEngine.NEW_TESTAMENT_BOOKS }; // Regular expressions private static readonly CMD_RE = /\/([^\s/]{1,24})/ug; private static readonly BIBLE_REF_RE = /^([가-힣A-Za-z]{1,6})(\d{0,3}):(\d{1,3})(?:-(\d{1,3}))?$/u; private constructor(apiEndpoint?: string, debounceMs?: number) { if (apiEndpoint) { this.apiEndpoint = apiEndpoint; } if (debounceMs !== undefined) { this.debounceMs = debounceMs; } } /** * Get singleton instance */ public static getInstance(apiEndpoint?: string, debounceMs?: number): BibleVerseEngine { if (!BibleVerseEngine.instance) { BibleVerseEngine.instance = new BibleVerseEngine(apiEndpoint, debounceMs); } return BibleVerseEngine.instance; } /** * Parse slash commands from text */ public parseSlashCommands(text: string): SlashCommandMatch[] { const matches: SlashCommandMatch[] = []; for (const match of text.matchAll(BibleVerseEngine.CMD_RE)) { const ref = match[1].replace(/\u00A0/g, ''); // Remove NBSP const position = match.index || 0; if (this.isValidBibleRef(ref)) { matches.push({ ref, position, fullMatch: match[0] }); } } return matches; } /** * Validate Bible reference format (supports range verses) */ public isValidBibleRef(ref: string): boolean { // Format check const match = ref.match(BibleVerseEngine.BIBLE_REF_RE); if (!match) return false; const [, , , fromVerse, toVerse] = match; // Range verse validation if (toVerse) { const from = parseInt(fromVerse); const to = parseInt(toVerse); // End verse must be greater than start verse if (to < from) { return false; } // Limit range to 10 verses maximum if (to - from > 10) { console.warn('📖 Range too large (max 10 verses):', ref); return false; } } // Book name validation const bookAbbr = ref.replace(/\s*\d.*$/, '').trim(); if (!BibleVerseEngine.ALL_BOOKS[bookAbbr]) { console.warn('📖 Unsupported Bible book:', bookAbbr); return false; } // Duplicate prevention (debouncing) const now = Date.now(); if (ref === this.lastInsertedRef && now - this.lastInsertedTime < this.debounceMs) { console.log('📖 Duplicate insertion prevented:', ref); return false; } console.log('📖 Valid Bible reference:', ref); return true; } /** * Load verse from API with caching and error handling */ public async loadVerse(ref: string): Promise<BibleVerseData | null> { // Check memory cache first const cachedVerse = this.cache[ref]; if (cachedVerse) { console.log('📖 Loading verse from cache:', ref, cachedVerse); return cachedVerse; } // API call if not cached try { const url = `${this.apiEndpoint}?query=${encodeURIComponent(ref)}`; const response = await fetch(url, { cache: 'force-cache' }); if (!response.ok) { // Handle different error status codes await response.json().catch(() => ({})); switch (response.status) { case 404: throw new BibleApiError('Verse not found', 404); case 422: throw new BibleApiError('Invalid verse format', 422); case 500: throw new BibleApiError('Server error', 500); default: throw new BibleApiError('Network error', response.status); } } const data: BibleApiResponse = await response.json(); // Handle new API response format (supports range verses) if (data.success && data.verses && data.verses.length > 0) { const verseData: BibleVerseData = { verses: data.verses, isRange: data.verses.length > 1 }; // Cache the result this.cache[ref] = verseData; console.log('📖 Verse loaded successfully:', ref, verseData); return verseData; } else { console.warn('📖 Verse not found:', ref); return null; } } catch (error) { if (error instanceof BibleApiError) { throw error; } console.error('📖 Network connection error:', ref, error); throw new BibleApiError('Network connection failed', undefined, error as Error); } } /** * Get book name from abbreviation */ public getBookName(abbr: string): string | undefined { return BibleVerseEngine.ALL_BOOKS[abbr]; } /** * Get all supported book abbreviations */ public getSupportedBooks(): string[] { return Object.keys(BibleVerseEngine.ALL_BOOKS); } /** * Update duplicate prevention state */ public updateInsertionState(ref: string): void { this.lastInsertedRef = ref; this.lastInsertedTime = Date.now(); } /** * Clear cache */ public clearCache(): void { this.cache = {}; console.log('📖 Bible verse cache cleared'); } /** * Get cache stats */ public getCacheStats(): { size: number; keys: string[] } { const keys = Object.keys(this.cache); return { size: keys.length, keys }; } /** * Set API endpoint */ public setApiEndpoint(endpoint: string): void { this.apiEndpoint = endpoint; } /** * Set debounce time */ public setDebounceMs(ms: number): void { this.debounceMs = ms; } /** * Extract book abbreviation from reference */ public extractBookAbbr(ref: string): string { return ref.replace(/\s*\d.*$/, '').trim(); } /** * Check if reference is a range verse */ public isRangeVerse(ref: string): boolean { const match = ref.match(BibleVerseEngine.BIBLE_REF_RE); return match ? !!match[4] : false; } /** * Parse verse numbers from reference */ public parseVerseNumbers(ref: string): { chapter: number; startVerse: number; endVerse?: number } | null { const match = ref.match(BibleVerseEngine.BIBLE_REF_RE); if (!match) return null; const [, , chapter, startVerse, endVerse] = match; return { chapter: parseInt(chapter) || 1, startVerse: parseInt(startVerse), endVerse: endVerse ? parseInt(endVerse) : undefined }; } }