import { labelFor as outlineLabel, kindLabel as outlineKindLabel } from './outlineLabels.js'; // --------------------------------------------------------------------------- // Study constants // --------------------------------------------------------------------------- export const CHAPTER_COUNTS = { GEN:50,EXO:40,LEV:27,NUM:36,DEU:34,JOS:24,JDG:21,RUT:4, '1SA':31,'2SA':24,'1KI':22,'2KI':25,'1CH':29,'2CH':36, EZR:10,NEH:13,EST:10,JOB:42,PSA:150,PRO:31,ECC:12,SNG:8, ISA:66,JER:52,LAM:5,EZK:48,DAN:12,HOS:14,JOL:3,AMO:9, OBA:1,JON:4,MIC:7,NAM:3,HAB:3,ZEP:3,HAG:2,ZEC:14,MAL:4, MAT:28,MRK:16,LUK:24,JHN:21,ACT:28,ROM:16,'1CO':16,'2CO':13, GAL:6,EPH:6,PHP:4,COL:4,'1TH':5,'2TH':3,'1TI':6,'2TI':4, TIT:3,PHM:1,HEB:13,JAS:5,'1PE':5,'2PE':3,'1JN':5,'2JN':1, '3JN':1,JUD:1,REV:22, }; export const bookOptions = [ { name: 'Genesis', abbrev: 'GEN' }, { name: 'Exodus', abbrev: 'EXO' }, { name: 'Leviticus', abbrev: 'LEV' }, { name: 'Numbers', abbrev: 'NUM' }, { name: 'Deuteronomy', abbrev: 'DEU' }, { name: 'Joshua', abbrev: 'JOS' }, { name: 'Judges', abbrev: 'JDG' }, { name: 'Ruth', abbrev: 'RUT' }, { name: '1 Samuel', abbrev: '1SA' }, { name: '2 Samuel', abbrev: '2SA' }, { name: '1 Kings', abbrev: '1KI' }, { name: '2 Kings', abbrev: '2KI' }, { name: '1 Chronicles', abbrev: '1CH' }, { name: '2 Chronicles', abbrev: '2CH' }, { name: 'Ezra', abbrev: 'EZR' }, { name: 'Nehemiah', abbrev: 'NEH' }, { name: 'Esther', abbrev: 'EST' }, { name: 'Job', abbrev: 'JOB' }, { name: 'Psalms', abbrev: 'PSA' }, { name: 'Proverbs', abbrev: 'PRO' }, { name: 'Ecclesiastes', abbrev: 'ECC' }, { name: 'Song', abbrev: 'SNG' }, { name: 'Isaiah', abbrev: 'ISA' }, { name: 'Jeremiah', abbrev: 'JER' }, { name: 'Lamentations', abbrev: 'LAM' }, { name: 'Ezekiel', abbrev: 'EZK' }, { name: 'Daniel', abbrev: 'DAN' }, { name: 'Hosea', abbrev: 'HOS' }, { name: 'Joel', abbrev: 'JOL' }, { name: 'Amos', abbrev: 'AMO' }, { name: 'Obadiah', abbrev: 'OBA' }, { name: 'Jonah', abbrev: 'JON' }, { name: 'Micah', abbrev: 'MIC' }, { name: 'Nahum', abbrev: 'NAM' }, { name: 'Habakkuk', abbrev: 'HAB' }, { name: 'Zephaniah', abbrev: 'ZEP' }, { name: 'Haggai', abbrev: 'HAG' }, { name: 'Zechariah', abbrev: 'ZEC' }, { name: 'Malachi', abbrev: 'MAL' }, { name: 'Matthew', abbrev: 'MAT' }, { name: 'Mark', abbrev: 'MRK' }, { name: 'Luke', abbrev: 'LUK' }, { name: 'John', abbrev: 'JHN' }, { name: 'Acts', abbrev: 'ACT' }, { name: 'Romans', abbrev: 'ROM' }, { name: '1 Corinthians', abbrev: '1CO' }, { name: '2 Corinthians', abbrev: '2CO' }, { name: 'Galatians', abbrev: 'GAL' }, { name: 'Ephesians', abbrev: 'EPH' }, { name: 'Philippians', abbrev: 'PHP' }, { name: 'Colossians', abbrev: 'COL' }, { name: '1 Thessalonians', abbrev: '1TH' }, { name: '2 Thessalonians', abbrev: '2TH' }, { name: '1 Timothy', abbrev: '1TI' }, { name: '2 Timothy', abbrev: '2TI' }, { name: 'Titus', abbrev: 'TIT' }, { name: 'Philemon', abbrev: 'PHM' }, { name: 'Hebrews', abbrev: 'HEB' }, { name: 'James', abbrev: 'JAS' }, { name: '1 Peter', abbrev: '1PE' }, { name: '2 Peter', abbrev: '2PE' }, { name: '1 John', abbrev: '1JN' }, { name: '2 John', abbrev: '2JN' }, { name: '3 John', abbrev: '3JN' }, { name: 'Jude', abbrev: 'JUD' }, { name: 'Revelation', abbrev: 'REV' }, ]; export const NT_BOOK_NUMBER = { MAT: 40, MRK: 41, LUK: 42, JHN: 43, ACT: 44, ROM: 45, '1CO': 46, '2CO': 47, GAL: 48, EPH: 49, PHP: 50, COL: 51, '1TH': 52, '2TH': 53, '1TI': 54, '2TI': 55, TIT: 56, PHM: 57, HEB: 58, JAS: 59, '1PE': 60, '2PE': 61, '1JN': 62, '2JN': 63, '3JN': 64, JUD: 65, REV: 66, }; export const storageKey = (translation, book, chapter) => `bible-study-${translation}-${book}-${chapter}`; export const makeId = () => crypto.randomUUID?.() ?? `${Date.now()}-${Math.random()}`; // --------------------------------------------------------------------------- // Format helpers // --------------------------------------------------------------------------- export function formatRelativeDate(ts) { if (!ts) return ''; const diff = Date.now() - ts; const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } export function chunkSpansNextChapter(project, startChapterIndex, chunk) { if (!project || !Array.isArray(project.chapters)) return false; if (!Number.isInteger(chunk?.spilloverEndVerse)) return false; if (startChapterIndex < 0 || startChapterIndex >= project.chapters.length - 1) return false; const startChapter = project.chapters[startChapterIndex]; const nextChapter = project.chapters[startChapterIndex + 1]; return startChapter?.bookAbbrev && startChapter.bookAbbrev === nextChapter?.bookAbbrev; } export function formatChunkReference(project, startChapterIndex, chunk, separator = '-') { if (!project || !chunk) return ''; const startChapter = project.chapters?.[startChapterIndex]; if (!startChapter) return ''; if (chunkSpansNextChapter(project, startChapterIndex, chunk)) { const nextChapter = project.chapters[startChapterIndex + 1]; return `${startChapter.book} ${startChapter.chapter}:${chunk.startVerse}${separator}${nextChapter.chapter}:${chunk.spilloverEndVerse}`; } if (chunk.startVerse === chunk.endVerse) { return `${startChapter.book} ${startChapter.chapter}:${chunk.startVerse}`; } return `${startChapter.book} ${startChapter.chapter}:${chunk.startVerse}${separator}${chunk.endVerse}`; } export function getChunkVerseEntries(project, startChapterIndex, chunk) { if (!project || !chunk) return []; const startChapter = project.chapters?.[startChapterIndex]; if (!startChapter) return []; const startVerses = (startChapter.verses ?? []) .filter((verse) => verse.number >= chunk.startVerse && verse.number <= chunk.endVerse) .map((verse) => ({ book: startChapter.book, chapter: startChapter.chapter, ...verse, })); if (!chunkSpansNextChapter(project, startChapterIndex, chunk)) { return startVerses; } const nextChapter = project.chapters[startChapterIndex + 1]; const spilloverVerses = (nextChapter.verses ?? []) .filter((verse) => verse.number >= 1 && verse.number <= chunk.spilloverEndVerse) .map((verse) => ({ book: nextChapter.book, chapter: nextChapter.chapter, ...verse, })); return [...startVerses, ...spilloverVerses]; } // --------------------------------------------------------------------------- // HTML export utilities // --------------------------------------------------------------------------- const escHtml = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); export function buildExportHtml(project) { const style = ` body { font-family: Georgia, serif; color: #0f172a; margin: 0; padding: 32px; } .page { max-width: 900px; margin: auto; } h1, h2 { font-family: Georgia, serif; } h1 { margin-bottom: 0.5rem; } .chapter-heading { margin: 2rem 0 0.5rem; font-size: 1.2rem; font-weight: 700; border-bottom: 1px solid #cbd5e1; padding-bottom: 0.25rem; } .chunk { margin-bottom: 2rem; padding: 1.25rem 1.5rem; border: 1px solid #cbd5e1; border-radius: 0.75rem; background: #ffffff; } .verse { margin: 0 0 0.75rem; line-height: 1.7; } .scripture-ref { font-weight: 700; margin-bottom: 0.75rem; } .oia, .cross-refs, .greek { margin-top: 1rem; } .oia-section { margin-bottom: 0.75rem; } .greek table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; } .greek th, .greek td { border: 1px solid #d1d5db; padding: 0.65rem; text-align: left; } .greek th { background: #f8fafc; } .definition { margin-top: 0.75rem; font-size: 0.95rem; line-height: 1.6; } .outline ul { list-style: none; margin: 0.25rem 0 0.25rem 1.5rem; padding: 0; } .outline > ul { margin-left: 0; } .outline li { margin: 0.3rem 0; line-height: 1.5; } .outline .lbl { display: inline-block; min-width: 2rem; font-weight: 700; } .outline .kind { font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.05em; color: #92400e; border: 1px solid #fcd34d; border-radius: 9999px; padding: 0 0.4em; margin-right: 0.3em; } .outline .o-note { margin: 0.2rem 0 0 2rem; color: #334155; font-size: 0.95em; } .outline .o-quote { margin: 0.3rem 0 0.3rem 2rem; padding-left: 0.75rem; border-left: 3px solid #cbd5e1; font-style: italic; } .outline .o-attach { margin-left: 2rem; font-size: 0.85em; color: #64748b; } .outline .ref { color: #64748b; font-size: 0.9em; } .definition-block { margin-top: 1rem; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.75rem; background: #f8fafc; } @media print { body { padding: 0; } .chunk { break-inside: avoid; border-color: #cbd5e1; } .chapter-heading { break-after: avoid; } } `; const chapters = Array.isArray(project.chapters) ? project.chapters : []; // The outline, if one was written: nested lists, labelled I. / A. / 1. like on the page const outlineHtml = (project.outline ?? []).length > 0 ? `

Outline

${outlineTimeHtml(project.outline)}${outlineListHtml(project.outline, 0)}
` : ''; const chunksHtml = chapters.map((ch, chapterIndex) => { const chapterHeader = `

${escHtml(ch.book)} ${escHtml(ch.chapter)}

`; const chunkSections = ch.chunks.map((chunk) => { const scripture = formatChunkReference(project, chapterIndex, chunk, '-'); const versesText = getChunkVerseEntries(project, chapterIndex, chunk) .map((verse) => `

${escHtml(verse.chapter)}:${escHtml(verse.number)} ${escHtml(verse.text)}

`) .join(''); const observation = escHtml((chunk.observation ?? '').trim()).replace(/\n/g, '
') || 'No observation.'; const interpretation = escHtml((chunk.interpretation ?? '').trim()).replace(/\n/g, '
') || 'No interpretation.'; const application = escHtml((chunk.application ?? '').trim()).replace(/\n/g, '
') || 'No application.'; const crossRefsHtml = (chunk.crossReferences ?? []).length > 0 ? `
CROSS-REFERENCES: ${chunk.crossReferences.map(escHtml).join(', ')}
` : ''; const greekRows = chunk.greekWords.map((word) => ` ${escHtml(word.strongNumber)} ${escHtml(word.lexeme)} ${escHtml(word.transliteration)} ${escHtml(word.partOfSpeech)} ${escHtml(word.shortDefinition)} `).join(''); const greekTable = wordTableHtml(greekRows); const extendedDefinitions = chunk.greekWords .map((word) => { if (word.definitionHtml) { return `

${escHtml(word.strongNumber)} — ${escHtml(word.lexeme)}

${word.definitionHtml}
`; } if ((word.shortDefinition === 'No definition found.' || word.shortDefinition === 'Lookup failed.') && !word.lexeme && !word.definitionHtml && word.query) { const raw = word.query.trim(); const normalized = /^\d+$/.test(raw) ? `G${raw}` : raw.toUpperCase(); const isStrongs = /^G\d+$/.test(normalized); const num = isStrongs ? normalized.slice(1) : null; const links = isStrongs && num ? `BibleHub · Blue Letter Bible · StudyLight` : `BibleHub · Blue Letter Bible`; return `

${escHtml(raw)} — definition not found in auto-lookup

Look it up manually: ${links}

`; } return ''; }) .join(''); return `
${escHtml(scripture)}
${versesText} ${(chunk.outline ?? []).length > 0 ? `
OUTLINE:${outlineListHtml(chunk.outline, 0)}
` : ''}
OBSERVATION:

${observation}

INTERPRETATION:

${interpretation}

APPLICATION:

${application}

${crossRefsHtml}
GREEK WORDS: ${greekRows ? greekTable : '

No Greek word notes.

'} ${extendedDefinitions}
`; }).join(''); return chapterHeader + chunkSections; }).join(''); return ` ${escHtml(project.title)}

${escHtml(project.title)}

${escHtml(project.translation)}

${outlineHtml} ${chunksHtml}
`; } //