// A one-page-per-session handout for a group: the passage, the outline, discussion questions and room to write. // It opens as a print-friendly page ("Save as PDF" in the print dialog). import { formatChunkReference, getChunkVerseEntries } from './bibleUtils.jsx'; import { flatten } from './outline.js'; const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const lines = (text) => String(text ?? '').split('\n').map((l) => l.replace(/^\s*(?:[-*•–]|\d{1,2}[.)])\s+/, '').trim()).filter(Boolean); // Every session (chunk) of the study, in order, with where it sits. export function sessionsFor(project) { const out = []; (project?.chapters ?? []).forEach((chapter, chapterIndex) => { for (const chunk of chapter.chunks ?? []) out.push({ chunk, chapterIndex, number: out.length + 1, reference: formatChunkReference(project, chapterIndex, chunk, '–') }); }); return out; } // The outline points for a session: its own outline, else the study outline's points linked to it. export function outlineForSession(project, chunk) { if ((chunk.outline ?? []).length) return chunk.outline; const linked = (project.outline ?? []).filter((n) => n.chunkId === chunk.id); return linked.length ? linked : []; } // The questions to print: the ones written for the session, else its Application notes as "Think about" prompts. export function questionsFor(chunk) { const own = lines(chunk.discussionQuestions); if (own.length) return { kind: 'questions', items: own }; const fallback = lines(chunk.application); return fallback.length ? { kind: 'think', items: fallback } : { kind: 'none', items: [] }; } const STOP = new Set(['the','and','but','for','nor','yet','who','whom','whose','that','this','these','those','with','from','into','unto','upon','shall','will','have','has','had','was','were','been','are','not','his','her','him','their','them','they','you','your','our','out','all','when','then','than','what','which','there','here','also','because','therefore','said','says']); // The words to leave blank in a fill-in passage: the English words you studied in this session (the word-study // glosses) that appear in it; if there are none, the longer words that repeat, since a repeated word is usually // what the passage is about. Returns lowercase words, most useful first, at most 12. export function blankWordsFor(chunk, verses) { const text = verses.map((v) => v.text).join(' '); const counts = new Map(); for (const w of text.toLowerCase().match(/[a-z]+(?:['’][a-z]+)?/g) ?? []) counts.set(w, (counts.get(w) ?? 0) + 1); const glossed = (chunk.greekWords ?? []).flatMap((w) => String(w.englishGloss ?? '').toLowerCase().match(/[a-z]+/g) ?? []) .filter((w) => w.length > 2 && !STOP.has(w) && counts.has(w)); const chosen = [...new Set(glossed)]; if (chosen.length === 0) { const repeated = [...counts].filter(([w, n]) => n >= 2 && w.length >= 5 && !STOP.has(w)).sort((a, b) => b[1] - a[1] || b[0].length - a[0].length).map(([w]) => w); chosen.push(...repeated); } return chosen.slice(0, 12); } // A verse's text with the chosen words replaced by a blank line of about the same length (each occurrence). export function blankOut(text, words) { const set = new Set(words); // split keeps the words (odd entries) apart from the punctuation between them, so both are escaped return String(text ?? '').split(/([A-Za-z]+(?:['’][A-Za-z]+)?)/).map((part, i) => ( i % 2 === 1 && set.has(part.toLowerCase()) ? `${' '.repeat(Math.max(8, part.length + 2))}` : esc(part) )).join(''); } // layout: 'standard' (everything), 'questions' (passage + questions only, one page), 'large' (big print), 'notes' (passage and a blank page to write). export const HANDOUT_LAYOUTS = [ { id: 'standard', label: 'Standard: passage, outline, questions' }, { id: 'questions', label: 'Questions only: passage and discussion questions' }, { id: 'large', label: 'Large print: passage and questions' }, { id: 'notes', label: 'Note-taking: passage and lots of lines' }, { id: 'blanks', label: 'Fill in the blanks: key words left out of the passage' }, ]; function sessionHtml(project, session, { answerLines, layout = 'standard' }) { const { chunk, chapterIndex, number, reference } = session; const series = (project.series ?? '').trim(); const title = (chunk.episodeTitle ?? '').trim(); const num = (chunk.episodeNumber ?? '').toString().trim() || number; const verses = getChunkVerseEntries(project, chapterIndex, chunk); const outline = layout === 'standard' ? flatten(outlineForSession(project, chunk)) : []; const q = layout === 'notes' ? { kind: 'none', items: [] } : questionsFor(chunk); const blanks = layout === 'blanks' ? blankWordsFor(chunk, verses) : []; const blank = (n) => Array.from({ length: n }, () => '
').join(''); return `

${esc(series || project.title)}

Session ${esc(num)}${title ? `: ${esc(title)}` : ''}

${esc(reference)} (${esc(project.translation ?? 'BSB')})

Name: ____________________________    Date: ________________

${verses.length ? `
${verses.map((v) => `

${esc(v.number)} ${layout === 'blanks' && blanks.length ? blankOut(v.text, blanks) : esc(v.text)}

`).join('')}
` : ''} ${blanks.length ? `

Word bank: ${[...blanks].sort().map(esc).join(' · ')}

` : ''} ${outline.length ? `

Outline

` : ''} ${q.items.length ? `

${q.kind === 'questions' ? 'Discussion questions' : 'Think about it'}

    ${q.items.map((item) => `
  1. ${esc(item)}${answerLines ? blank(2) : ''}
  2. `).join('')}
` : ''} ${answerLines || layout === 'notes' ? `

Notes

${blank(layout === 'notes' ? 16 : q.items.length ? 4 : 10)}` : ''}
`; } // ids: the chunk ids to include, or null for every session export function buildHandoutHtml(project, ids = null, { answerLines = true, layout = 'standard' } = {}) { const sessions = sessionsFor(project).filter((s) => !ids || ids.includes(s.chunk.id)); return `${esc(project.title)} — handout${sessions.map((s) => sessionHtml(project, s, { answerLines, layout })).join('')}`; } export function printHandout(project, ids = null, options) { const win = window.open('', '_blank'); if (!win) return false; win.document.write(buildHandoutHtml(project, ids, options)); win.document.close(); win.onload = () => { win.focus(); win.print(); }; return true; }