Outline: per-chunk outlines, richer points, print and share

- One outline for the whole study plus one per chunk (picker at the top);
  chunk outlines can start from the chunk's verses, each carrying its words
- Points can have a type (illustration, application, quote, question,
  transition), minutes (with a running total), a longer note, verse text
  fetched from a passage, and cross-references / word studies attached from
  the chunk
- Share (system share sheet, or copy), Print / PDF, Copy as text, Word and
  Markdown downloads for any one outline
- All of it included in the Markdown, HTML/PDF and Word study exports,
  with each chunk's outline under that chunk

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 14:24:28 -04:00
1 parent d273c8cb3e
commit 820886e3ab
12 files changed
+741 -103

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { attachableFromChunk, fetchPassageText, parsePassageRange } from './passageText.js';
const chapter = { chapter: { content: [1, 2, 3, 4, 5].map((n) => ({ type: 'verse', number: n, content: [`Verse ${n} text.`] })) } };
beforeEach(() => vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(chapter) }))));
afterEach(() => vi.unstubAllGlobals());
test('reads a verse, a range, or a whole chapter', () => {
expect(parsePassageRange('Titus 1:5')).toMatchObject({ book: 'TIT', chapter: 1, start: 5, end: 5 });
expect(parsePassageRange('titus 1:5-9')).toMatchObject({ start: 5, end: 9 });
expect(parsePassageRange('1 Cor 13:4–7')).toMatchObject({ book: '1CO', chapter: 13, start: 4, end: 7 });
expect(parsePassageRange('John 3')).toMatchObject({ chapter: 3, start: null });
});
test('rejects things that are not passages, or backwards ranges', () => {
expect(parsePassageRange('hello')).toBeNull();
expect(parsePassageRange('Titus 9:1')).toBeNull();
expect(parsePassageRange('Titus 1:9-5')).toBeNull();
});
test('fetches just the verses asked for, and names the passage', async () => {
const got = await fetchPassageText('Titus 1:2-3');
expect(got).toEqual({ ref: 'Titus 1:2–3', text: 'Verse 2 text. Verse 3 text.', translation: 'BSB' });
});
test('says so when the verses are not there, or the passage is not understood', async () => {
await expect(fetchPassageText('Titus 1:50')).rejects.toThrow(/weren’t found/);
await expect(fetchPassageText('nonsense')).rejects.toThrow(/Type a passage/);
});
test('offers a chunk\'s cross-references and studied words to attach', () => {
const chunk = {
crossReferences: ['John 1:1'],
greekWords: [
{ strongNumber: 'G1401', lexeme: 'δοῦλος', shortDefinition: 'a slave' },
{ query: 'xyz', shortDefinition: 'No definition found.' },
],
};
expect(attachableFromChunk(chunk)).toEqual([
{ key: 'x:John 1:1', type: 'xref', label: 'John 1:1' },
{ key: 'w:G1401', type: 'word', label: 'δοῦλος (G1401)', detail: 'a slave' },
{ key: 'w:xyz', type: 'word', label: 'xyz', detail: undefined },
]);
expect(attachableFromChunk(null)).toEqual([]);
});