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

+58 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest';
import {
addAfter, addChild, addRoot, countNodes, flatten, getNode, indent, labelFor, moveNode, newNode, outdent,
insertAt, locate, outlineToMarkdown, outlineToText, removeNode, seedFromChunks, toRoman, updateNode, MAX_DEPTH,
STUDY, chunkOf, detailLines, insertAt, outlineFor, seedFromVerses, withOutline, locate, timeSummary, totalMinutes, outlineToMarkdown, outlineToText, removeNode, seedFromChunks, toRoman, updateNode, MAX_DEPTH,
} from './outline.js';
const n = (id, text = id, children = []) => ({ id, text, ref: '', chunkId: null, children });
@@ -120,3 +120,60 @@ describe('putting a deleted point back', () => {
expect(ids(insertAt([n('b')], 'missing', 0, n('x')))).toEqual(['x', 'b']);
});
});
describe('richer points', () => {
const rich = [{
...n('a', 'Creation'), ref: 'Gen 1', kind: 'illustration', minutes: 5, note: 'Tell the story.\n\nKeep it short.',
scripture: { ref: 'Genesis 1:1', text: 'In the beginning', translation: 'BSB' },
attachments: [{ id: 'x', type: 'xref', label: 'John 1:1' }, { id: 'w', type: 'word', label: 'bara (H1254)', detail: 'to create' }],
children: [{ ...n('b', 'Light'), minutes: 3 }],
}];
test('text and markdown show the kind, minutes, note, verse and attachments', () => {
expect(outlineToText(rich)).toBe([
'I. [Illustration] Creation (Gen 1) · 5 min',
' Tell the story.',
' Keep it short.',
' “In the beginning” — Genesis 1:1',
' See: John 1:1',
' Word: bara (H1254) — to create',
' A. Light · 3 min',
].join('\n'));
expect(outlineToMarkdown(rich)).toContain('- **I.** [Illustration] Creation (Gen 1) · 5 min\n *Tell the story.*');
});
test('totals the minutes, and says nothing when there are none', () => {
expect(totalMinutes(rich)).toBe(8);
expect(timeSummary(rich)).toBe('About 8 minutes');
expect(timeSummary([n('z')])).toBe('');
});
test('new points start plain, and older ones without the new fields still work', () => {
expect(newNode('x')).toMatchObject({ note: '', kind: '', minutes: null, scripture: null, attachments: [] });
expect(detailLines(n('old'))).toEqual([]);
});
});
describe('an outline for each chunk', () => {
const project = {
translation: 'BSB', outline: [n('whole')],
chapters: [{ book: 'Titus', bookAbbrev: 'TIT', chapter: '1', verses: [{ number: 1, text: 'One.' }, { number: 2, text: 'Two.' }, { number: 3, text: 'Three.' }], chunks: [
{ id: 'c1', startVerse: 1, endVerse: 2, observation: '', outline: [n('mine')] },
{ id: 'c2', startVerse: 3, endVerse: 3, observation: '' },
] }],
};
test('reads and replaces the outline of the whole study or of one chunk', () => {
expect(ids(outlineFor(project, STUDY))).toEqual(['whole']);
expect(ids(outlineFor(project, 'c1'))).toEqual(['mine']);
expect(outlineFor(project, 'c2')).toEqual([]);
const next = withOutline(project, 'c2', [n('new')]);
expect(ids(outlineFor(next, 'c2'))).toEqual(['new']);
expect(ids(outlineFor(next, 'c1'))).toEqual(['mine']);
expect(ids(outlineFor(next, STUDY))).toEqual(['whole']);
expect(outlineFor(project, 'c2')).toEqual([]); // the original is untouched
expect(ids(outlineFor(withOutline(project, STUDY, []), STUDY))).toEqual([]);
});
test('seeding from verses adds a point per verse with its words, and never twice', () => {
const t = seedFromVerses(project, 'c1', []);
expect(t.map((x) => [x.ref, x.scripture.text])).toEqual([['Titus 1:1', 'One.'], ['Titus 1:2', 'Two.']]);
expect(seedFromVerses(project, 'c1', t)).toHaveLength(2);
expect(seedFromVerses(project, 'c2', [])).toHaveLength(1);
});
});