export type Bookmark = { id: string; title: string; url: string; notes: string; folders: string[]; }; export type Group = { url: string; items: Bookmark[]; reason: string }; export type Reading = { id: string; title: string; url: string; originals: string[]; folders: string[][]; topics: string[]; score: number; evidence: string[]; }; export type Progress = { schemaVersion: 1; order: string[]; completed: string[]; }; export function importBookmarks(input: unknown): Bookmark[] { const result: Bookmark[] = []; const ids = new Set(); function walk(nodes: unknown, folders: string[], depth: number) { if (depth > 32) throw new Error("Folder nesting exceeds 32"); for (const node of array(nodes, "bookmarks")) { if (!node || typeof node !== "object") throw new Error("Bookmark must be an object"); if ("children" in node) { walk( node.children, [...folders, nonempty(node.title, "folder title")], depth + 1, ); continue; } const id = nonempty(node.id, "id"), title = nonempty(node.title, "title"), url = nonempty(node.url, "url"); normalizeURL(url); if (ids.has(id)) throw new Error("Duplicate id " + id); ids.add(id); if (node.notes !== undefined && typeof node.notes !== "string") throw new Error("notes must be text"); result.push({ id, title, url, notes: node.notes ?? "", folders: [...folders], }); } } walk(input, [], 0); return result; } export function normalizeURL(value: string): string { const u = new URL(nonempty(value, "url")); if (!["http:", "https:"].includes(u.protocol) || u.username || u.password) throw new Error("Only HTTP(S) URLs without credentials are supported"); u.hash = ""; return u.href; } export function groupDuplicates(items: Bookmark[]): Group[] { const groups = new Map(); for (const item of items) { const key = normalizeURL(item.url); groups.set(key, [...(groups.get(key) ?? []), structuredClone(item)]); } return [...groups].map(([url, items]) => ({ url, items, reason: items.length > 1 ? "Same WHATWG URL after removing the fragment; query parameters remain significant." : "Unique normalized document URL.", })); } export function readingPath( groups: Group[], topics: Record, ): Reading[] { const entries = Object.entries(topics); for (const [name, words] of entries) { nonempty(name, "topic"); if ( !Array.isArray(words) || !words.length || words.some((w) => typeof w !== "string" || !w.trim()) ) throw new Error("Each topic needs nonempty keywords"); } const tokens = (s: string) => new Set(s.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []); return groups .map((group) => { let score = 0; const matched: string[] = [], evidence: string[] = []; for (const [topic, words] of entries) { let local = 0; for (const word of [...new Set(words.map((w) => w.toLowerCase()))]) { const title = group.items.some((x) => tokens(x.title).has(word)), notes = group.items.some((x) => tokens(x.notes).has(word)); local += (title ? 3 : 0) + (notes ? 1 : 0); if (title || notes) evidence.push( `${topic}: ${word} = ${title ? 3 : 0} title + ${notes ? 1 : 0} notes`, ); } if (local) matched.push(topic); score += local; } return { id: group.items[0].id, title: group.items[0].title, url: group.url, originals: group.items.map((x) => x.url), folders: group.items.map((x) => x.folders), topics: matched, score, evidence, }; }) .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)); } export function applyProgress( items: Reading[], value?: unknown, ): { items: Reading[]; progress: Progress } { const ids = items.map((x) => x.id); let order = ids, completed: string[] = []; if (value !== undefined) { const p = value as Progress; if ( !p || p.schemaVersion !== 1 || !Array.isArray(p.order) || p.order.length !== ids.length || new Set(p.order).size !== ids.length || p.order.some((x) => !ids.includes(x)) ) throw new Error( "Progress order must contain every current id exactly once", ); if ( !Array.isArray(p.completed) || new Set(p.completed).size !== p.completed.length || p.completed.some((x) => !ids.includes(x)) ) throw new Error("Unknown or duplicate completed id"); order = p.order; completed = p.completed; } return { items: order.map((id) => structuredClone(items.find((x) => x.id === id)!)), progress: { schemaVersion: 1, order: [...order], completed: [...completed], }, }; } export function renderCollection(items: Reading[], progress: Progress): string { return page( "Bookmark Path Organizer", `

A reading path with original links, retained folders and explicit topic evidence. Move items and download your progress for the next run.

`, `const items=${embedded(items)},state=${embedded(progress)};const root=document.querySelector('#items');function render(){root.replaceChildren();for(const id of state.order){const item=items.find(x=>x.id===id),row=document.createElement('article'),a=document.createElement('a');a.href=item.url;a.textContent=item.title;a.rel='noopener';row.append(a);const evidence=document.createElement('p');evidence.textContent='Score '+item.score+' · '+(item.evidence.join('; ')||'No topic match');row.append(evidence);const original=document.createElement('p');original.textContent='Originals: '+item.originals.join(' | ')+' · Folders: '+item.folders.map(x=>x.join('/')).join(' | ');row.append(original);const label=document.createElement('label'),check=document.createElement('input');check.type='checkbox';check.checked=state.completed.includes(id);check.onchange=()=>{state.completed=check.checked?[...state.completed,id]:state.completed.filter(x=>x!==id)};label.append(check,' Read');row.append(label);for(const [text,d]of [['Move up',-1],['Move down',1]]){const b=document.createElement('button');b.textContent=text;b.disabled=state.order.indexOf(id)+d<0||state.order.indexOf(id)+d>=state.order.length;b.onclick=()=>{const i=state.order.indexOf(id);[state.order[i],state.order[i+d]]=[state.order[i+d],state.order[i]];render()};row.append(b)}root.append(row)}}document.querySelector('#save').onclick=()=>download('progress.json',state);render();`, ); } export function escapeHTML(value: unknown): string { return String(value).replace( /[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ c ]!, ); } export function embedded(value: unknown): string { return JSON.stringify(value) .replace(/${escapeHTML(title)}

AI Engineering from Scratch · local project

${escapeHTML(title)}

${content}
`; } export function array(value: unknown, label: string): any[] { if (!Array.isArray(value)) throw new Error(label + " must be an array"); return value; } export function nonempty(value: unknown, label: string): string { if (typeof value !== "string" || !value.trim()) throw new Error(label + " must be a nonempty string"); return value; }