// Bookmarks ({ "JHN-3-16": "#fef08a" }) and text highlights ([{ id, book, chapter, verse, // startOffset, endOffset, color }]) — helpers for keeping them in sync between devices. export const emptyMarks = () => ({ bookmarks: {}, highlights: [] }); export const isEmptyMarks = (m) => Object.keys(m.bookmarks).length === 0 && m.highlights.length === 0; // Forgiving read of whatever came from localStorage or the server. export function normalizeMarks(raw) { const bookmarks = raw?.bookmarks && typeof raw.bookmarks === 'object' && !Array.isArray(raw.bookmarks) ? raw.bookmarks : {}; const highlights = Array.isArray(raw?.highlights) ? raw.highlights.filter((h) => h && typeof h.id === 'string') : []; return { bookmarks, highlights }; } const sameSpan = (a, b) => a.book === b.book && a.chapter === b.chapter && a.verse === b.verse && a.startOffset === b.startOffset && a.endOffset === b.endOffset; // Union of two devices' marks. Used only when both changed since they last synced, or the // first time a device meets the server's copy, so nothing made on either side is lost. Where the // same bookmark has two colours, the server's wins. export function mergeMarks(local, server) { const bookmarks = { ...local.bookmarks, ...server.bookmarks }; const highlights = [...server.highlights]; for (const h of local.highlights) { if (!highlights.some((s) => s.id === h.id || sameSpan(s, h))) highlights.push(h); } return { bookmarks, highlights }; } export function marksEqual(a, b) { const ka = Object.keys(a.bookmarks); if (ka.length !== Object.keys(b.bookmarks).length) return false; if (ka.some((k) => a.bookmarks[k] !== b.bookmarks[k])) return false; if (a.highlights.length !== b.highlights.length) return false; const byId = new Map(b.highlights.map((h) => [h.id, h])); return a.highlights.every((h) => { const o = byId.get(h.id); return o && sameSpan(h, o) && h.color === o.color; }); }