初始化项目
This commit is contained in:
41
packages/shared/src/bookmarkHtml.js
Normal file
41
packages/shared/src/bookmarkHtml.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Netscape Bookmark file format parser (Chrome/Edge export)
|
||||
// Parses <DL>/<DT><H3> folders and <DT><A> bookmarks.
|
||||
|
||||
export function parseNetscapeBookmarkHtml(html) {
|
||||
// Minimal, dependency-free parser using DOMParser (works in browsers).
|
||||
// For Node.js, server will use a separate HTML parser.
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
const rootDl = doc.querySelector("dl");
|
||||
if (!rootDl) return { folders: [], bookmarks: [] };
|
||||
|
||||
const folders = [];
|
||||
const bookmarks = [];
|
||||
let folderIdSeq = 1;
|
||||
|
||||
function walkDl(dl, parentFolderId) {
|
||||
const children = Array.from(dl.children);
|
||||
for (const node of children) {
|
||||
if (node.tagName?.toLowerCase() !== "dt") continue;
|
||||
|
||||
const h3 = node.querySelector(":scope > h3");
|
||||
const a = node.querySelector(":scope > a");
|
||||
const nextDl = node.nextElementSibling?.tagName?.toLowerCase() === "dl" ? node.nextElementSibling : null;
|
||||
|
||||
if (h3) {
|
||||
const id = String(folderIdSeq++);
|
||||
const name = (h3.textContent || "").trim();
|
||||
folders.push({ id, parentFolderId: parentFolderId ?? null, name });
|
||||
if (nextDl) walkDl(nextDl, id);
|
||||
} else if (a) {
|
||||
const title = (a.textContent || "").trim();
|
||||
const url = a.getAttribute("href") || "";
|
||||
bookmarks.push({ parentFolderId: parentFolderId ?? null, title, url });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDl(rootDl, null);
|
||||
return { folders, bookmarks };
|
||||
}
|
||||
49
packages/shared/src/index.js
Normal file
49
packages/shared/src/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export function normalizeUrl(input) {
|
||||
try {
|
||||
const url = new URL(input);
|
||||
url.hash = "";
|
||||
|
||||
url.protocol = url.protocol.toLowerCase();
|
||||
url.hostname = url.hostname.toLowerCase();
|
||||
|
||||
// Remove default ports
|
||||
if ((url.protocol === "http:" && url.port === "80") || (url.protocol === "https:" && url.port === "443")) {
|
||||
url.port = "";
|
||||
}
|
||||
|
||||
// Trim trailing slash on pathname (but keep root '/')
|
||||
if (url.pathname.length > 1 && url.pathname.endsWith("/")) {
|
||||
url.pathname = url.pathname.slice(0, -1);
|
||||
}
|
||||
|
||||
// Drop common tracking params
|
||||
const trackingPrefixes = ["utm_", "spm", "gclid", "fbclid"];
|
||||
for (const key of [...url.searchParams.keys()]) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (trackingPrefixes.some((p) => lowerKey.startsWith(p))) {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort params for stable output
|
||||
const sorted = [...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b));
|
||||
url.search = "";
|
||||
for (const [k, v] of sorted) url.searchParams.append(k, v);
|
||||
|
||||
return url.toString();
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
export function computeUrlHash(normalizedUrl) {
|
||||
// Lightweight hash (non-crypto) for dedupe key; server may replace with crypto later.
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < normalizedUrl.length; i++) {
|
||||
hash ^= normalizedUrl.charCodeAt(i);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
|
||||
export { parseNetscapeBookmarkHtml } from "./bookmarkHtml.js";
|
||||
3
packages/shared/src/package.json
Normal file
3
packages/shared/src/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"private": true
|
||||
}
|
||||
Reference in New Issue
Block a user