From 9fbc152a91a0d5fac98aefbcdcb782aebacaa4e2 Mon Sep 17 00:00:00 2001 From: Eugene Fox Date: Fri, 19 Dec 2025 23:10:28 +0300 Subject: [PATCH] feat: add netscape bookmark import/export #203 --- entrypoints/options/hooks/useOptionsStyles.ts | 4 + .../options/layouts/StorageSection.tsx | 104 ++++++----- .../layouts/BookmarksSection.tsx | 66 +++++++ .../utils/convertBookmarks.ts | 99 +++++++++++ .../utils/exportBookmarks.ts | 65 +++++++ .../utils/importBookmarks.ts | 52 ++++++ locales/en.yml | 13 ++ locales/es.yml | 13 ++ locales/it.yml | 13 ++ locales/pl.yml | 13 ++ locales/pt_BR.yml | 13 ++ locales/ru.yml | 13 ++ locales/uk.yml | 19 +- locales/zh_CN.yml | 13 ++ package-lock.json | 162 ++++++++++++++++++ package.json | 1 + 16 files changed, 613 insertions(+), 50 deletions(-) create mode 100644 features/netscapeBookmarks/layouts/BookmarksSection.tsx create mode 100644 features/netscapeBookmarks/utils/convertBookmarks.ts create mode 100644 features/netscapeBookmarks/utils/exportBookmarks.ts create mode 100644 features/netscapeBookmarks/utils/importBookmarks.ts diff --git a/entrypoints/options/hooks/useOptionsStyles.ts b/entrypoints/options/hooks/useOptionsStyles.ts index 1304f5d..2de1dc0 100644 --- a/entrypoints/options/hooks/useOptionsStyles.ts +++ b/entrypoints/options/hooks/useOptionsStyles.ts @@ -41,5 +41,9 @@ export const useOptionsStyles = makeStyles({ flexFlow: "column", alignItems: "flex-start", gap: tokens.spacingVerticalSNudge + }, + messageBar: + { + flexShrink: 0 } }); diff --git a/entrypoints/options/layouts/StorageSection.tsx b/entrypoints/options/layouts/StorageSection.tsx index c30753e..4ba05c6 100644 --- a/entrypoints/options/layouts/StorageSection.tsx +++ b/entrypoints/options/layouts/StorageSection.tsx @@ -2,12 +2,13 @@ import { useDialog } from "@/contexts/DialogProvider"; import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage"; import { useDangerStyles } from "@/hooks/useDangerStyles"; import useStorageInfo from "@/hooks/useStorageInfo"; -import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components"; +import { Button, Divider, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Subtitle2, Switch } from "@fluentui/react-components"; import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons"; import { Unwatch } from "wxt/utils/storage"; import { useOptionsStyles } from "../hooks/useOptionsStyles"; import exportData from "../utils/exportData"; import importData from "../utils/importData"; +import BookmarksSection from "@/features/netscapeBookmarks/layouts/BookmarksSection"; export default function StorageSection(): React.ReactElement { @@ -78,6 +79,59 @@ export default function StorageSection(): React.ReactElement return ( <> + { i18n.t("options_page.storage.manage_title") } + + { isCloudDisabled === false && + = 0.8 ? "error" : undefined } + > + + + } + +
+ { isCloudDisabled === true && + + } + + { isCloudDisabled === false && +
+ +
+ } +
+ +
+ + +
+ + { importResult !== null && + + + { importResult === true ? + i18n.t("options_page.storage.import_results.success") : + i18n.t("options_page.storage.import_results.error") + } + + + } + + + { i18n.t("options_page.storage.thumbnails_title") }
- { isCloudDisabled === false && - = 0.8 ? "error" : undefined } - > - - - } - - { isCloudDisabled === true && - - } - -
- - -
- - { importResult !== null && - - - { importResult === true ? - i18n.t("options_page.storage.import_results.success") : - i18n.t("options_page.storage.import_results.error") - } - - - } - - { isCloudDisabled === false && -
- -
- } + + ); } diff --git a/features/netscapeBookmarks/layouts/BookmarksSection.tsx b/features/netscapeBookmarks/layouts/BookmarksSection.tsx new file mode 100644 index 0000000..bd13b56 --- /dev/null +++ b/features/netscapeBookmarks/layouts/BookmarksSection.tsx @@ -0,0 +1,66 @@ +import { useDialog } from "@/contexts/DialogProvider"; +import { Body1, Button, makeStyles, MessageBar, MessageBarBody, Subtitle2, tokens } from "@fluentui/react-components"; +import { ArrowDownload24Regular, ArrowUpload24Regular } from "@fluentui/react-icons"; +import importBookmarks from "../utils/importBookmarks"; +import exportBookmarks from "../utils/exportBookmarks"; + +export default function BookmarksSection(): React.ReactElement +{ + const cls = useStyles(); + const dialog = useDialog(); + + const [importResult, setImportResult] = useState(null); + + const handleImport = (): void => + dialog.pushPrompt({ + title: i18n.t("features.netscape_bookmarks.import_dialog.title"), + confirmText: i18n.t("options_page.storage.import_prompt.proceed"), + onConfirm: () => importBookmarks().then(setImportResult), + content: ( + + { i18n.t("features.netscape_bookmarks.import_dialog.content") } + + ) + }); + + return ( +
+ { i18n.t("features.netscape_bookmarks.title") } + + { importResult !== null && + = 0 ? "success" : "error" } layout="multiline"> + + { importResult >= 0 ? + i18n.t("features.netscape_bookmarks.import_result.success", [importResult]) : + i18n.t("features.netscape_bookmarks.import_result.error") + } + + + } + +
+ + +
+
+ ); +} + +const useStyles = makeStyles({ + root: + { + display: "flex", + flexFlow: "column", + gap: tokens.spacingVerticalMNudge + }, + buttons: + { + display: "flex", + flexWrap: "wrap", + gap: tokens.spacingVerticalSNudge + } +}); diff --git a/features/netscapeBookmarks/utils/convertBookmarks.ts b/features/netscapeBookmarks/utils/convertBookmarks.ts new file mode 100644 index 0000000..294f528 --- /dev/null +++ b/features/netscapeBookmarks/utils/convertBookmarks.ts @@ -0,0 +1,99 @@ +import { CollectionItem, GraphicsStorage, GroupItem, TabItem } from "@/models/CollectionModels"; +import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark"; + +export default function convertBookmarks(bookmarks: Bookmark[]): [CollectionItem[], GraphicsStorage, number] +{ + let count: number = 0; + const graphics: GraphicsStorage = {}; + const items: CollectionItem[] = []; + const untitled: CollectionItem = { + items: [], + timestamp: Date.now(), + type: "collection" + }; + + for (const bookmark of bookmarks) + { + if (bookmark.type === "bookmark") + { + untitled.items.push(getTab(bookmark, graphics)); + count++; + } + else if (bookmark.type === "folder" && bookmark.children) + { + const collection: CollectionItem = getCollection(bookmark, graphics); + items.push(collection); + count += collection.items.reduce((acc, item) => + { + if (item.type === "tab") + return acc + 1; + else if (item.type === "group") + return acc + item.items.length; + return acc; + }, 0); + } + } + + return [items, graphics, count]; +} + +function getTab(bookmark: Bookmark, graphics: GraphicsStorage): TabItem +{ + if (bookmark.icon) + graphics[bookmark.url!] = { + icon: bookmark.icon + }; + + return { + type: "tab", + url: bookmark.url!, + title: bookmark.title || bookmark.url! + }; +} + +function getCollection(bookmark: Bookmark, graphics: GraphicsStorage): CollectionItem +{ + const collection: CollectionItem = { + items: [], + title: bookmark.title, + timestamp: Date.now(), + type: "collection" + }; + + for (const child of bookmark.children!) + { + if (child.type === "bookmark") + collection.items.push(getTab(child, graphics)); + else if (child.type === "folder" && child.children) + collection.items.push(getGroup(child, graphics)); + } + + return collection; +} + +function getGroup(bookmark: Bookmark, graphics: GraphicsStorage): GroupItem +{ + const group: GroupItem = { + items: [], + title: bookmark.title, + pinned: false, + type: "group", + color: getRandomColor() + }; + + for (const child of bookmark.children!) + { + if (child.type === "bookmark") + group.items.push(getTab(child, graphics)); + else if (child.type === "folder") + group.items.push(...getGroup(child, graphics).items); + } + + return group; +} + +function getRandomColor(): "blue" | "cyan" | "green" | "grey" | "orange" | "pink" | "purple" | "red" | "yellow" +{ + const colors = ["blue", "cyan", "green", "grey", "orange", "pink", "purple", "red", "yellow"] as const; + return colors[Math.floor(Math.random() * colors.length)]; +} diff --git a/features/netscapeBookmarks/utils/exportBookmarks.ts b/features/netscapeBookmarks/utils/exportBookmarks.ts new file mode 100644 index 0000000..cfaf62e --- /dev/null +++ b/features/netscapeBookmarks/utils/exportBookmarks.ts @@ -0,0 +1,65 @@ +import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle"; +import { getCollections } from "@/features/collectionStorage"; +import { CollectionItem, GroupItem } from "@/models/CollectionModels"; + +export default async function exportBookmarks(): Promise +{ + const [collections] = await getCollections(); + const lines: string[] = [ + "", + "", + "", + "Bookmarks", + "

Bookmarks

", + "

" + ]; + + for (const collection of collections) + lines.push(...createFolder(collection)); + + lines.push("

"); + + const data: string = lines.join("\n"); + + const blob: Blob = new Blob([data], { type: "text/html" }); + + const element: HTMLAnchorElement = document.createElement("a"); + element.style.display = "none"; + element.href = URL.createObjectURL(blob); + element.setAttribute("download", "collections.html"); + + document.body.appendChild(element); + element.click(); + + URL.revokeObjectURL(element.href); + document.body.removeChild(element); +} + +function createFolder(item: CollectionItem | GroupItem): string[] +{ + const lines: string[] = []; + const title: string = item.type === "collection" ? + (item.title ?? getCollectionTitle(item)) : + (item.pinned ? i18n.t("groups.pinned") : (item.title ?? "")); + + lines.push(`

${sanitizeString(title)}

`); + lines.push("

"); + + for (const subItem of item.items) + { + if (subItem.type === "tab") + lines.push(`

${sanitizeString(subItem.title || subItem.url)}`); + else if (subItem.type === "group") + lines.push(...createFolder(subItem)); + } + + lines.push("

"); + return lines; +} + +function sanitizeString(str: string): string +{ + return str.replace(/&/g, "&").replace(//g, ">"); +} diff --git a/features/netscapeBookmarks/utils/importBookmarks.ts b/features/netscapeBookmarks/utils/importBookmarks.ts new file mode 100644 index 0000000..5729663 --- /dev/null +++ b/features/netscapeBookmarks/utils/importBookmarks.ts @@ -0,0 +1,52 @@ +import { getCollections, saveCollections } from "@/features/collectionStorage"; +import { sendMessage } from "@/utils/messaging"; +import parse from "node-bookmarks-parser"; +import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark"; +import convertBookmarks from "./convertBookmarks"; + +export default async function importBookmarks(): Promise +{ + const element: HTMLInputElement = document.createElement("input"); + element.style.display = "none"; + element.hidden = true; + element.type = "file"; + element.accept = ".html"; + + document.body.appendChild(element); + element.click(); + + await new Promise(resolve => + { + const listener = () => + { + element.removeEventListener("input", listener); + resolve(null); + }; + element.addEventListener("input", listener); + }); + + if (!element.files || element.files.length < 1) + return null; + + const file: File = element.files[0]; + const content: string = await file.text(); + + document.body.removeChild(element); + + try + { + const bookmarks: Bookmark[] = parse(content); + const [data, graphics, tabCount] = convertBookmarks(bookmarks); + const [collections, cloudIssues] = await getCollections(); + + await saveCollections([...data, ...collections], cloudIssues === null, graphics); + sendMessage("refreshCollections", undefined); + + return tabCount; + } + catch (error) + { + console.error("Failed to parse bookmarks file", error); + return -1; + } +} diff --git a/locales/en.yml b/locales/en.yml index bc2c6ae..9c22fae 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -46,6 +46,17 @@ features: p3_text: "See the full list of what we collect" p3_link: "here" + netscape_bookmarks: + title: "Browser bookmarks" + export: "Export collections as bookmarks" + import: "Import from bookmarks file" + import_dialog: + title: "Import bookmarks" + content: "Import bookmarks from a Netscape-format bookmarks file exported from your browser." + import_result: + success: "Successfully imported $1 bookmarks." + error: "Failed to import bookmarks. Please ensure the file is a valid bookmarks file." + notifications: tabs_saved: title: "New collection created" @@ -114,6 +125,8 @@ options_page: restore: "Open tabs and remove the collection" storage: title: "Storage" + manage_title: "Storage management" + thumbnails_title: "Thumbnails & icons" capacity: title: "Cloud storage capacity" description: "$1 of $2 KiB" diff --git a/locales/es.yml b/locales/es.yml index 0ade1e4..4d63dd5 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -46,6 +46,17 @@ features: p3_text: "Ver la lista completa de lo que recopilamos" p3_link: "aquí" + netscape_bookmarks: + title: "Marcadores del navegador" + export: "Exportar colecciones como marcadores" + import: "Importar desde archivo de marcadores" + import_dialog: + title: "Importar marcadores" + content: "Importa marcadores desde un archivo de marcadores en formato Netscape exportado desde tu navegador." + import_result: + success: "Se importaron correctamente $1 marcadores." + error: "No se pudieron importar los marcadores. Asegúrate de que el archivo sea un archivo de marcadores válido." + notifications: tabs_saved: title: "Nueva colección creada" @@ -114,6 +125,8 @@ options_page: restore: "Abrir pestañas y eliminar la colección" storage: title: "Almacenamiento" + manage_title: "Administrar almacenamiento" + thumbnails_title: "Miniaturas e íconos" capacity: title: "Capacidad de almacenamiento en la nube" description: "$1 de $2 KiB" diff --git a/locales/it.yml b/locales/it.yml index 6c0942f..515f93a 100644 --- a/locales/it.yml +++ b/locales/it.yml @@ -46,6 +46,17 @@ features: p3_text: "Vedi l'elenco completo di ciò che raccogliamo" p3_link: "qui" + netscape_bookmarks: + title: "Segnalibri del browser" + export: "Esporta collezioni come segnalibri" + import: "Importa da file di segnalibri" + import_dialog: + title: "Importa segnalibri" + content: "Importa segnalibri da un file di segnalibri in formato Netscape esportato dal tuo browser." + import_result: + success: "Importati con successo $1 segnalibri." + error: "Impossibile importare i segnalibri. Assicurati che il file sia un file di segnalibri valido." + notifications: tabs_saved: title: "Nuova collezione creata" @@ -114,6 +125,8 @@ options_page: restore: "Apri le schede e rimuovi la collezione" storage: title: "Archiviazione" + manage_title: "Gestisci archiviazione" + thumbnails_title: "Miniature e icone" capacity: title: "Capacità di archiviazione cloud" description: "$1 di $2 KiB" diff --git a/locales/pl.yml b/locales/pl.yml index d413f2a..702da79 100644 --- a/locales/pl.yml +++ b/locales/pl.yml @@ -46,6 +46,17 @@ features: p3_text: "Pełną listę zbieranych danych można zobaczyć" p3_link: "tutaj" + netscape_bookmarks: + title: "Import/eksport zakładek" + export: "Eksportuj kolekcje jako plik zakładek" + import: "Importuj z pliku zakładek" + import_dialog: + title: "Import zakładek" + content: "Importuj zakładki z pliku zakładek w formacie Netscape wyeksportowanego z przeglądarki." + import_result: + success: "Zakładki zostały pomyślnie zaimportowane ($1)" + error: "Nie udało się zaimportować zakładek. Upewnij się, że plik jest poprawnym plikiem zakładek." + notifications: tabs_saved: title: "Utworzono nową kolekcję" @@ -114,6 +125,8 @@ options_page: restore: "Otwórz karty i usuń kolekcję" storage: title: "Magazyn" + manage_title: "Zarządzaj magazynem" + thumbnails_title: "Podglądy i ikony" capacity: title: "Magazyn w chmurze" description: "$1 z $2 KiB" diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index ec231df..544a307 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -46,6 +46,17 @@ features: p3_text: "Veja a lista completa do que coletamos" p3_link: "aqui" + netscape_bookmarks: + title: "Favoritos do navegador" + export: "Exportar coleções como favoritos" + import: "Importar de arquivo de favoritos" + import_dialog: + title: "Importar favoritos" + content: "Importe favoritos de um arquivo de favoritos no formato Netscape exportado do seu navegador." + import_result: + success: "Importados com sucesso $1 favoritos." + error: "Falha ao importar favoritos. Por favor, certifique-se de que o arquivo é um arquivo de favoritos válido." + notifications: tabs_saved: title: "Nova coleção criada" @@ -114,6 +125,8 @@ options_page: restore: "Abrir abas e remover a coleção" storage: title: "Armazenamento" + manage_title: "Gerenciar armazenamento" + thumbnails_title: "Miniaturas e ícones" capacity: title: "Capacidade de armazenamento na nuvem" description: "$1 de $2 KiB" diff --git a/locales/ru.yml b/locales/ru.yml index a61f4c1..9aec98e 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -46,6 +46,17 @@ features: p3_text: "Полный список собираемых данных можно посмотреть" p3_link: "здесь" + netscape_bookmarks: + title: "Импорт/экспорт закладок" + export: "Экспортировать коллекции как файл закладок" + import: "Импорт из файла закладок" + import_dialog: + title: "Импорт закладок" + content: "Импортируйте закладки из файла закладок в формате Netscape, экспортированного из вашего браузера." + import_result: + success: "Закладки успешно импортированы ($1 шт.)" + error: "Не удалось импортировать закладки. Пожалуйста, убедитесь, что файл является допустимым файлом закладок." + notifications: tabs_saved: title: "Создана новая коллекция" @@ -114,6 +125,8 @@ options_page: restore: "Открыть вкладки и удалить коллекцию" storage: title: "Хранилище" + manage_title: "Управление хранилищем" + thumbnails_title: "Превью и иконки" capacity: title: "Объём облачного хранилища" description: "$1 из $2 КиБ" diff --git a/locales/uk.yml b/locales/uk.yml index 4ff8342..d4fc708 100644 --- a/locales/uk.yml +++ b/locales/uk.yml @@ -46,6 +46,17 @@ features: p3_text: "Повний список зібраних даних можна подивитися" p3_link: "тут" + netscape_bookmarks: + title: "Імпорт/експорт закладок" + export: "Експортувати колекції як файл закладок" + import: "Імпорт із файлу закладок" + import_dialog: + title: "Імпорт закладок" + content: "Імпортуйте закладки з файлу закладок у форматі Netscape, експортованого з вашого браузера." + import_result: + success: "Закладки успішно імпортовані ($1 шт.)" + error: "Не вдалося імпортувати закладки. Будь ласка, переконайтеся, що файл є коректним файлом закладок." + notifications: tabs_saved: title: "Створено нову колекцію" @@ -114,6 +125,8 @@ options_page: restore: "Відкрити вкладки та видалити колекцію" storage: title: "Сховище" + manage_title: "Керування сховищем" + thumbnails_title: "Прев'ю та іконки" capacity: title: "Хмарне сховище" description: "$1 з $2 КіБ" @@ -132,13 +145,13 @@ options_page: disable_prompt: text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі." action: "Вимкнути та перезавантажити розширення" - thumbnail_capture: "Зберігати превью і іконки для збережених вкладок" + thumbnail_capture: "Зберігати прев'ю і іконки для збережених вкладок" thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів" thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок" clear_thumbnails: action: "Видалити збережені іконки" - title: "Видалити превью і іконки?" - prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати." + title: "Видалити прев'ю і іконки?" + prompt: "Ця дія видалить всі прев'ю і іконки у ваших збережених вкладках. Цю дію не можна скасувати." about: title: "О розширенні" developed_by: "Розробник: Євген Лис" diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml index 4ecc1e5..82cb6b1 100644 --- a/locales/zh_CN.yml +++ b/locales/zh_CN.yml @@ -46,6 +46,17 @@ features: p3_text: "请参阅我们收集内容的" p3_link: "完整列表" + netscape_bookmarks: + title: "浏览器书签" + export: "将收藏导出为书签" + import: "从书签文件导入" + import_dialog: + title: "导入书签" + content: "从您的浏览器导出的 Netscape 格式书签文件中导入书签。" + import_result: + success: "成功导入 $1 个书签。" + error: "导入书签失败。请确保该文件是有效的书签文件。" + notifications: tabs_saved: title: "已创建新收藏" @@ -114,6 +125,8 @@ options_page: restore: "打开标签页并删除收藏" storage: title: "存储" + manage_title: "存储管理" + thumbnails_title: "缩略图和图标" capacity: title: "云存储容量" description: "$1 / $2 KiB" diff --git a/package-lock.json b/package-lock.json index 091687b..5350e41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@wxt-dev/analytics": "^0.5.1", "@wxt-dev/i18n": "^0.2.4", "lzutf8": "^0.6.3", + "node-bookmarks-parser": "^2.0.0", "react": "^19.2.1", "react-dom": "^19.2.1" }, @@ -4595,6 +4596,48 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -5412,6 +5455,19 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6675,6 +6731,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8080,6 +8148,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-bookmarks-parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-bookmarks-parser/-/node-bookmarks-parser-2.0.0.tgz", + "integrity": "sha512-BHQpogPEifFP+eToF+GS6cP9DInDsh++c3PFtMiH0oJ1ByeEXcoZw0joDio1sIpm1lJAFyY6msguZ5ZJTEueZg==", + "license": "MIT", + "dependencies": { + "cheerio": "^1.0.0-rc.3" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -8550,6 +8627,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9287,6 +9413,12 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sax": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", @@ -10201,6 +10333,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -10526,6 +10667,27 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/when": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz", diff --git a/package.json b/package.json index e68e7ff..54d8593 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@wxt-dev/analytics": "^0.5.1", "@wxt-dev/i18n": "^0.2.4", "lzutf8": "^0.6.3", + "node-bookmarks-parser": "^2.0.0", "react": "^19.2.1", "react-dom": "^19.2.1" },