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

+79 -7
View File
@@ -1,14 +1,18 @@
// A study's true outline: main points, sub-points under them, and so on (I. / A. / 1. / a.). It is a tree
// of nodes { id, text, ref, chunkId, children } kept on the project as `project.outline`. Every function
// returns a new tree and leaves the one it was given alone.
import { formatChunkReference, makeId } from './bibleUtils.jsx';
import { labelFor, toRoman } from './outlineLabels.js';
import { formatChunkReference, getChunkVerseEntries, makeId } from './bibleUtils.jsx';
import { KINDS, kindLabel, labelFor, toRoman } from './outlineLabels.js';
export { labelFor, toRoman };
export { KINDS, kindLabel, labelFor, toRoman };
export const MAX_DEPTH = 5; // levels below the main points
export const newNode = (text = '', extra = {}) => ({ id: makeId(), text, ref: '', chunkId: null, children: [], ...extra });
// A point: its words, an optional passage and chunk link, plus a longer note, a kind, minutes,
// the text of a verse, and things attached from the chunks (cross-references, word studies).
export const newNode = (text = '', extra = {}) => ({
id: makeId(), text, ref: '', chunkId: null, note: '', kind: '', minutes: null, scripture: null, attachments: [], children: [], ...extra,
});
// ---- finding ---------------------------------------------------------------
@@ -136,14 +140,44 @@ export function flatten(tree, depth = 0, parentId = null) {
export const countNodes = (tree) => flatten(tree).length;
const pointText = (n) => `${n.text.trim() || '(untitled)'}${n.ref.trim() ? ` (${n.ref.trim()})` : ''}`;
const minutesText = (m) => (Number.isFinite(m) && m > 0 ? `${m} min` : '');
// Total minutes written on the points (blank ones count as nothing)
export const totalMinutes = (tree) => flatten(tree).reduce((sum, { node }) => sum + (Number.isFinite(node.minutes) ? node.minutes : 0), 0);
// The headline of a point: "[Illustration] The words (passage) · 5 min"
const pointText = (n) => {
const kind = n.kind ? `[${kindLabel(n.kind)}] ` : '';
const mins = minutesText(n.minutes);
return `${kind}${(n.text ?? '').trim() || '(untitled)'}${(n.ref ?? '').trim() ? ` (${n.ref.trim()})` : ''}${mins ? ` · ${mins}` : ''}`;
};
// The lines under a point's headline: its note, verse text and attachments.
export function detailLines(node) {
const lines = [];
for (const l of (node.note ?? '').split('\n')) if (l.trim()) lines.push(l.trim());
if (node.scripture?.text) lines.push(`“${node.scripture.text}” — ${node.scripture.ref}`);
for (const a of node.attachments ?? []) lines.push(`${a.type === 'word' ? 'Word' : 'See'}: ${a.label}${a.detail ? ` — ${a.detail}` : ''}`);
return lines;
}
export const timeSummary = (tree) => {
const total = totalMinutes(tree);
return total > 0 ? `About ${total} minute${total === 1 ? '' : 's'}` : '';
};
export function outlineToText(tree) {
return flatten(tree).map(({ node, depth, label }) => `${' '.repeat(depth)}${label} ${pointText(node)}`).join('\n');
return flatten(tree).flatMap(({ node, depth, label }) => [
`${' '.repeat(depth)}${label} ${pointText(node)}`,
...detailLines(node).map((l) => `${' '.repeat(depth)} ${l}`),
]).join('\n');
}
export function outlineToMarkdown(tree) {
return flatten(tree).map(({ node, depth, label }) => `${' '.repeat(depth)}- **${label}** ${pointText(node)}`).join('\n');
return flatten(tree).flatMap(({ node, depth, label }) => [
`${' '.repeat(depth)}- **${label}** ${pointText(node)}`,
...detailLines(node).map((l) => `${' '.repeat(depth)} *${l}*`),
]).join('\n');
}
// ---- starting one ------------------------------------------------------------
@@ -162,3 +196,41 @@ export function seedFromChunks(project, tree = []) {
});
return [...tree, ...added];
}
// ---- one outline per chunk ---------------------------------------------------
// A study has one outline for the whole message (project.outline) and each chunk can have its own
// (chunk.outline). `scope` is STUDY or a chunk's id.
export const STUDY = 'study';
export function chunkOf(project, chunkId) {
for (const chapter of project?.chapters ?? []) for (const chunk of chapter.chunks ?? []) if (chunk.id === chunkId) return chunk;
return null;
}
export const outlineFor = (project, scope) => (scope === STUDY ? project?.outline ?? [] : chunkOf(project, scope)?.outline ?? []);
// A copy of the project with that scope's outline replaced.
export function withOutline(project, scope, tree) {
if (scope === STUDY) return { ...project, outline: tree };
return {
...project,
chapters: project.chapters.map((ch) => ({ ...ch, chunks: ch.chunks.map((c) => (c.id === scope ? { ...c, outline: tree } : c)) })),
};
}
// One point per verse of the chunk, each carrying that verse's words, for verses not already in `tree`.
export function seedFromVerses(project, chunkId, tree = []) {
const have = new Set(flatten(tree).map((f) => f.node.scripture?.ref).filter(Boolean));
const added = [];
(project?.chapters ?? []).forEach((chapter, chapterIndex) => {
for (const chunk of chapter.chunks ?? []) {
if (chunk.id !== chunkId) continue;
for (const v of getChunkVerseEntries(project, chapterIndex, chunk)) {
const ref = `${chapter.book} ${v.chapter}:${v.number}`;
if (have.has(ref)) continue;
added.push(newNode('', { ref, scripture: { ref, text: v.text, translation: project.translation ?? 'BSB' } }));
}
}
});
return [...tree, ...added];
}