Files
study-bible-project/server/outlineShare.js
nmemmertandClaude Sonnet 5.5 d7a1601f80 Outline: share as a read-only link
- Share on the Outline page opens a sheet: create / copy / turn off a
  read-only link, share the link with the system share sheet, or send the
  outline as text
- The link has its own token (separate from the whole-study share link) and
  the public endpoint returns only the outlines, rebuilt field by field:
  no observations, interpretations, applications, scripts or word notes
- Opening the link shows the outline(s) with no sign-in, a switcher between
  the whole-study and per-chunk outlines, and Print / PDF
- New outline_share_token column (added automatically on start)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:02:40 -04:00

60 lines
3.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// What a read-only outline link shows: the outlines of a study (the whole-study one and each chunk's)
// and nothing else — no observations, interpretations, applications, scripts or word notes. The
// outline data comes from the browser, so it is rebuilt field by field instead of passed through.
const MAX_NODES = 2000;
const MAX_DEPTH = 8;
const KINDS = new Set(['', 'illustration', 'application', 'quote', 'question', 'transition']);
const str = (v, max) => (typeof v === 'string' ? v.slice(0, max) : '');
function cleanTree(nodes, depth, counter) {
if (!Array.isArray(nodes) || depth > MAX_DEPTH) return [];
const out = [];
for (const n of nodes) {
if (!n || typeof n !== 'object' || counter.n >= MAX_NODES) continue;
counter.n += 1;
const minutes = Number.isFinite(n.minutes) && n.minutes >= 0 && n.minutes <= 600 ? n.minutes : null;
out.push({
id: str(n.id, 64) || `n${counter.n}`,
text: str(n.text, 2000),
ref: str(n.ref, 200),
kind: KINDS.has(n.kind) ? n.kind : '',
minutes,
note: str(n.note, 8000),
scripture: n.scripture && typeof n.scripture.text === 'string'
? { ref: str(n.scripture.ref, 200), text: str(n.scripture.text, 6000), translation: str(n.scripture.translation, 12) }
: null,
attachments: (Array.isArray(n.attachments) ? n.attachments : []).slice(0, 50)
.filter((a) => a && typeof a === 'object')
.map((a) => ({ id: str(a.id, 100), type: a.type === 'word' ? 'word' : 'xref', label: str(a.label, 200), ...(a.detail ? { detail: str(a.detail, 500) } : {}) })),
children: cleanTree(n.children, depth + 1, counter),
});
}
return out;
}
const chunkLabel = (chapters, chapterIndex, chunk) => {
const ch = chapters[chapterIndex];
const next = chapters[chapterIndex + 1];
if (Number.isInteger(chunk.spilloverEndVerse) && next && next.bookAbbrev === ch.bookAbbrev) {
return `${ch.book} ${ch.chapter}:${chunk.startVerse}–${next.chapter}:${chunk.spilloverEndVerse}`;
}
return chunk.startVerse === chunk.endVerse ? `${ch.book} ${ch.chapter}:${chunk.startVerse}` : `${ch.book} ${ch.chapter}:${chunk.startVerse}–${chunk.endVerse}`;
};
// -> { title, translation, outlines: [{ scope, label, tree }] } with only the outlines that have points
export function buildOutlinePayload(project) {
const counter = { n: 0 };
const outlines = [];
const whole = cleanTree(project?.outline, 0, counter);
if (whole.length > 0) outlines.push({ scope: 'study', label: 'Whole study', tree: whole });
const chapters = Array.isArray(project?.chapters) ? project.chapters : [];
chapters.forEach((ch, chapterIndex) => {
for (const chunk of Array.isArray(ch?.chunks) ? ch.chunks : []) {
const tree = cleanTree(chunk?.outline, 0, counter);
if (tree.length > 0) outlines.push({ scope: str(chunk.id, 64), label: str(chunkLabel(chapters, chapterIndex, chunk), 200), tree });
}
});
return { title: str(project?.title, 500), translation: str(project?.translation, 12) || 'BSB', outlines };
}