Teaching tools: word occurrences, blanks handout, templates, series progress, book info, comments
Build and Push Docker Image / build-and-push (push) Successful in 6m41s
Build and Push Docker Image / build-and-push (push) Successful in 6m41s
- Words tab: "Where else" lists every verse using a Strong's number, by book (new /api/strongs/:number/occurrences, indexed from the interlinear files) - Handout: fill-in-the-blanks layout with a word bank - Four new study templates: character, topical, word, verse by verse - Finish: series progress (not started / studied / ready / taught) - Background: book at a glance for all 66 books - Chunk comments for co-teachers, merged between copies - TODO: correct BSB audio note, tick co-teacher comments Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d9891ff9fe
commit
a6af9bd1c6
24 files changed
+670
-9
No files matched your search
+34
-1
@@ -30,12 +30,41 @@ export function questionsFor(chunk) {
|
||||
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()) ? `<span class="blank">${' '.repeat(Math.max(8, part.length + 2))}</span>` : 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' }) {
|
||||
@@ -46,6 +75,7 @@ function sessionHtml(project, session, { answerLines, layout = 'standard' }) {
|
||||
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 }, () => '<div class="rule"></div>').join('');
|
||||
return `<section class="page${layout === 'standard' ? '' : ` ${layout}`}">
|
||||
<header>
|
||||
@@ -54,7 +84,8 @@ function sessionHtml(project, session, { answerLines, layout = 'standard' }) {
|
||||
<p class="ref">${esc(reference)} <span>(${esc(project.translation ?? 'BSB')})</span></p>
|
||||
<p class="meta">Name: ____________________________ Date: ________________</p>
|
||||
</header>
|
||||
${verses.length ? `<div class="passage">${verses.map((v) => `<p><sup>${esc(v.number)}</sup> ${esc(v.text)}</p>`).join('')}</div>` : ''}
|
||||
${verses.length ? `<div class="passage">${verses.map((v) => `<p><sup>${esc(v.number)}</sup> ${layout === 'blanks' && blanks.length ? blankOut(v.text, blanks) : esc(v.text)}</p>`).join('')}</div>` : ''}
|
||||
${blanks.length ? `<p class="bank"><b>Word bank:</b> ${[...blanks].sort().map(esc).join(' · ')}</p>` : ''}
|
||||
${outline.length ? `<h2>Outline</h2><ul class="outline">${outline.map(({ node, depth, label }) => `<li style="margin-left:${depth * 1.2}em"><b>${esc(label)}</b> ${esc(node.text || '')}${node.ref ? ` <i>(${esc(node.ref)})</i>` : ''}</li>`).join('')}</ul>` : ''}
|
||||
${q.items.length ? `<h2>${q.kind === 'questions' ? 'Discussion questions' : 'Think about it'}</h2><ol class="questions">${q.items.map((item) => `<li>${esc(item)}${answerLines ? blank(2) : ''}</li>`).join('')}</ol>` : ''}
|
||||
${answerLines || layout === 'notes' ? `<h2>Notes</h2>${blank(layout === 'notes' ? 16 : q.items.length ? 4 : 10)}` : ''}
|
||||
@@ -78,6 +109,8 @@ export function buildHandoutHtml(project, ids = null, { answerLines = true, layo
|
||||
.passage { background: #f6f1e6; border-left: 3px solid #a84e27; padding: .4em 1em; } .passage p { margin: .35em 0; } sup { color: #a84e27; font-size: 70%; font-family: -apple-system, Helvetica, Arial, sans-serif; }
|
||||
.outline { list-style: none; padding: 0; margin: 0; } .outline li { margin-bottom: .25em; } .outline b { color: #a84e27; }
|
||||
.questions { padding-left: 1.4em; } .questions li { margin-bottom: .7em; }
|
||||
.blank { display: inline-block; border-bottom: 1.5px solid #1f1b16; line-height: 1; vertical-align: baseline; }
|
||||
.bank { font: 10.5pt -apple-system, Helvetica, Arial, sans-serif; color: #6b6458; margin: .6em 0 0; }
|
||||
.rule { border-bottom: 1px solid #c9bfa8; height: 1.7em; }
|
||||
.large { font-size: 17pt; line-height: 1.55; } .large h1 { font-size: 30pt; } .large .rule { height: 2.2em; }
|
||||
footer { position: absolute; bottom: 0; left: 0; right: 0; text-align: center; color: #9b9484; font: 9pt -apple-system, Helvetica, Arial, sans-serif; }
|
||||
|
||||
Reference in new issue
Block a user