94 lines
3.1 KiB
JavaScript
94 lines
3.1 KiB
JavaScript
|
|
import * as cheerio from "cheerio";
|
||
|
|
|
||
|
|
export function parseNetscapeBookmarkHtmlNode(html) {
|
||
|
|
const $ = cheerio.load(html, { decodeEntities: false });
|
||
|
|
const rootDl = $("dl").first();
|
||
|
|
if (!rootDl.length) return { folders: [], bookmarks: [] };
|
||
|
|
|
||
|
|
const folders = [];
|
||
|
|
const bookmarks = [];
|
||
|
|
|
||
|
|
function walkDl($dl, parentTempId) {
|
||
|
|
// Netscape format: <DL><p> contains repeating <DT> items and nested <DL>
|
||
|
|
const children = $dl.children().toArray();
|
||
|
|
for (let i = 0; i < children.length; i++) {
|
||
|
|
const node = children[i];
|
||
|
|
if (!node || node.tagName?.toLowerCase() !== "dt") continue;
|
||
|
|
|
||
|
|
const $dt = $(node);
|
||
|
|
const $h3 = $dt.children("h3").first();
|
||
|
|
const $a = $dt.children("a").first();
|
||
|
|
const $next = $(children[i + 1] || null);
|
||
|
|
const nextIsDl = $next && $next[0]?.tagName?.toLowerCase() === "dl";
|
||
|
|
|
||
|
|
if ($h3.length) {
|
||
|
|
const tempId = `${folders.length + 1}`;
|
||
|
|
const name = ($h3.text() || "").trim();
|
||
|
|
folders.push({ tempId, parentTempId: parentTempId ?? null, name });
|
||
|
|
if (nextIsDl) walkDl($next, tempId);
|
||
|
|
} else if ($a.length) {
|
||
|
|
const title = ($a.text() || "").trim();
|
||
|
|
const url = $a.attr("href") || "";
|
||
|
|
bookmarks.push({ parentTempId: parentTempId ?? null, title, url });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
walkDl(rootDl, null);
|
||
|
|
return { folders, bookmarks };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildNetscapeBookmarkHtml({ folders, bookmarks }) {
|
||
|
|
// folders: [{id, parentId, name}]
|
||
|
|
// bookmarks: [{folderId, title, url}]
|
||
|
|
const folderChildren = new Map();
|
||
|
|
const bookmarkChildren = new Map();
|
||
|
|
|
||
|
|
for (const f of folders) {
|
||
|
|
const key = f.parentId ?? "root";
|
||
|
|
if (!folderChildren.has(key)) folderChildren.set(key, []);
|
||
|
|
folderChildren.get(key).push(f);
|
||
|
|
}
|
||
|
|
for (const b of bookmarks) {
|
||
|
|
const key = b.folderId ?? "root";
|
||
|
|
if (!bookmarkChildren.has(key)) bookmarkChildren.set(key, []);
|
||
|
|
bookmarkChildren.get(key).push(b);
|
||
|
|
}
|
||
|
|
|
||
|
|
function esc(s) {
|
||
|
|
return String(s)
|
||
|
|
.replaceAll("&", "&")
|
||
|
|
.replaceAll("<", "<")
|
||
|
|
.replaceAll(">", ">")
|
||
|
|
.replaceAll('"', """);
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderFolder(parentId) {
|
||
|
|
const key = parentId ?? "root";
|
||
|
|
const subFolders = (folderChildren.get(key) || []).slice().sort((a, b) => a.name.localeCompare(b.name));
|
||
|
|
const subBookmarks = (bookmarkChildren.get(key) || []).slice().sort((a, b) => a.title.localeCompare(b.title));
|
||
|
|
|
||
|
|
let out = "<DL><p>\n";
|
||
|
|
|
||
|
|
for (const f of subFolders) {
|
||
|
|
out += ` <DT><H3>${esc(f.name)}</H3>\n`;
|
||
|
|
out += renderFolder(f.id)
|
||
|
|
.split("\n")
|
||
|
|
.map((line) => (line ? ` ${line}` : line))
|
||
|
|
.join("\n");
|
||
|
|
out += "\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const b of subBookmarks) {
|
||
|
|
out += ` <DT><A HREF=\"${esc(b.url)}\">${esc(b.title)}</A>\n`;
|
||
|
|
}
|
||
|
|
|
||
|
|
out += "</DL><p>";
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
const header = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is an automatically generated file. -->\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n`;
|
||
|
|
const body = renderFolder(null);
|
||
|
|
return header + body + "\n";
|
||
|
|
}
|