Notes: multiple notes per section with last-edited dates, delete, sort by recently edited; Word export now readable; shared note-format module + tests; v2.43.0
Publish Container / build (push) Successful in 16m3s
Publish Container / build (push) Successful in 16m3s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
401fd451de
commit
09177f47ed
8 files changed
+285
-76
No files matched your search
@@ -0,0 +1,79 @@
|
||||
// Lesson note storage format, shared by the client and the server.
|
||||
//
|
||||
// Each lesson's notes are stored as one string. Current format (v2):
|
||||
// {"v":2,"entries":[{"id","anchor","html","updatedAt"}]}
|
||||
// Legacy format (v1) was a JSON object of { anchor: html }, and older still was
|
||||
// plain text. Both are still read and are upgraded to v2 on the next save.
|
||||
|
||||
export const NOTE_ANCHORS = ['scripture', 'commentary', 'questions', 'general']
|
||||
|
||||
export const NOTE_ANCHOR_LABELS = {
|
||||
scripture: 'Scripture Text',
|
||||
commentary: 'Commentary',
|
||||
questions: 'Reflection Questions',
|
||||
general: 'General Notes',
|
||||
}
|
||||
|
||||
export function newNoteId() {
|
||||
return `n${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`
|
||||
}
|
||||
|
||||
export function parseNoteEntries(raw) {
|
||||
if (typeof raw !== 'string' || !raw.trim()) return []
|
||||
let parsed = null
|
||||
try {
|
||||
parsed = JSON.parse(raw)
|
||||
} catch {
|
||||
// Legacy plain-text note.
|
||||
return [{ id: newNoteId(), anchor: 'general', html: raw, updatedAt: null }]
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return [{ id: newNoteId(), anchor: 'general', html: raw, updatedAt: null }]
|
||||
}
|
||||
if (parsed.v === 2 && Array.isArray(parsed.entries)) {
|
||||
return parsed.entries
|
||||
.filter(e => e && typeof e.html === 'string' && e.html.trim())
|
||||
.map(e => ({
|
||||
id: typeof e.id === 'string' && e.id ? e.id : newNoteId(),
|
||||
anchor: NOTE_ANCHORS.includes(e.anchor) ? e.anchor : 'general',
|
||||
html: e.html,
|
||||
updatedAt: typeof e.updatedAt === 'string' ? e.updatedAt : null,
|
||||
}))
|
||||
}
|
||||
return Object.entries(parsed)
|
||||
.filter(([, html]) => typeof html === 'string' && html.trim())
|
||||
.map(([anchor, html]) => ({
|
||||
id: `legacy-${anchor}`,
|
||||
anchor: NOTE_ANCHORS.includes(anchor) ? anchor : 'general',
|
||||
html,
|
||||
updatedAt: null,
|
||||
}))
|
||||
}
|
||||
|
||||
export function serializeNoteEntries(entries) {
|
||||
const kept = entries.filter(e => e.html && e.html.trim())
|
||||
if (kept.length === 0) return ''
|
||||
return JSON.stringify({ v: 2, entries: kept })
|
||||
}
|
||||
|
||||
export function noteHtmlToText(html) {
|
||||
return String(html)
|
||||
.replace(/<\/(p|li|h[1-6]|blockquote)>/gi, '\n')
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, '&')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
// Plain-text rendering of a whole lesson's notes (used by the Word export).
|
||||
export function noteEntriesToText(raw) {
|
||||
return parseNoteEntries(raw)
|
||||
.map(e => `${NOTE_ANCHOR_LABELS[e.anchor]}\n${noteHtmlToText(e.html)}`)
|
||||
.join('\n\n')
|
||||
}
|
||||
Reference in new issue
Block a user