export interface BookmarkLite { title: string; url: string; description?: string; icon?: string; category?: string; sortOrder?: number; } export function hostnameOf(raw: string): string { try { const u = new URL(raw.includes("://") ? raw : `https://${raw}`); return u.hostname.replace(/^www\./, ""); } catch { return ""; } } export function faviconFallback(url: string, size = 64): string { const host = hostnameOf(url); if (!host) return ""; return `https://www.google.com/s2/favicons?domain=${host}&sz=${size}`; } export function normalizeURL(raw: string): string { const trimmed = raw.trim(); if (!trimmed) return ""; if (/^https?:\/\//i.test(trimmed)) return trimmed; return `https://${trimmed}`; } export function exportToJSON(bookmarks: BookmarkLite[]): string { return JSON.stringify( { exportedAt: new Date().toISOString(), bookmarks: bookmarks.map((b) => ({ title: b.title, url: b.url, description: b.description ?? "", icon: b.icon ?? "", category: b.category ?? "默认", sortOrder: b.sortOrder ?? 0, })), }, null, 2, ); } export function downloadFile(filename: string, content: string, mime = "application/json") { const blob = new Blob([content], { type: `${mime};charset=utf-8` }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } export function parseImport(text: string): BookmarkLite[] { const trimmed = text.trim(); if (!trimmed) return []; if (trimmed.startsWith("{") || trimmed.startsWith("[")) { return parseJSON(trimmed); } if (trimmed.toLowerCase().includes(" { const i = item as Record; return { title: String(i.title ?? i.name ?? "").trim(), url: String(i.url ?? i.href ?? "").trim(), description: i.description ? String(i.description) : "", icon: i.icon ? String(i.icon) : "", category: i.category ? String(i.category) : "默认", sortOrder: typeof i.sortOrder === "number" ? i.sortOrder : 0, }; }) .filter((b) => b.title && b.url); } catch { return []; } } function parseNetscapeHTML(raw: string): BookmarkLite[] { if (typeof DOMParser === "undefined") return []; const doc = new DOMParser().parseFromString(raw, "text/html"); const out: BookmarkLite[] = []; const walk = (node: Element, currentCategory: string) => { const children = Array.from(node.children); for (let i = 0; i < children.length; i++) { const el = children[i]; const tag = el.tagName.toLowerCase(); if (tag === "dt") { const inner = el.firstElementChild; if (!inner) continue; const innerTag = inner.tagName.toLowerCase(); if (innerTag === "a") { const a = inner as HTMLAnchorElement; const href = a.getAttribute("href") || ""; const title = (a.textContent || "").trim() || href; const icon = a.getAttribute("icon") || ""; if (href && /^https?:\/\//i.test(href)) { out.push({ title, url: href, description: "", icon, category: currentCategory || "默认", sortOrder: 0, }); } } else if (innerTag === "h3") { const folderName = (inner.textContent || "").trim() || currentCategory; const dl = el.querySelector("dl"); if (dl) walk(dl, folderName); } } else if (tag === "dl") { walk(el, currentCategory); } } }; const root = doc.querySelector("dl"); if (root) walk(root, "默认"); return out; }