├── .gitattributes ├── .gitea ├── scripts │ └── tag-changelog-config.cjs └── workflows │ └── release.yaml ├── .github ├── FUNDING.yml ├── scripts │ └── tag-changelog-config.cjs └── workflows │ └── release.yaml ├── .gitignore ├── .mocharc.cjs ├── .versionrc.json ├── LICENSE ├── README.md ├── README_CN.md ├── auto-imports.d.ts ├── components.d.ts ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── src ├── assets │ ├── _locales │ │ ├── en │ │ │ └── messages.json │ │ ├── zh_CN │ │ │ └── messages.json │ │ └── zh_TW │ │ │ └── messages.json │ └── icons │ │ └── app │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-19.png │ │ ├── icon-24.png │ │ ├── icon-32.png │ │ ├── icon-48.png │ │ ├── icon-64.png │ │ └── icon-96.png ├── background │ └── index.ts ├── common │ ├── i18n.ts │ └── tools.ts ├── components │ └── BSettingsDrawer.vue ├── img │ └── icon-24.png ├── index │ ├── App.vue │ ├── index.html │ └── index.ts ├── manifest │ ├── chrome │ │ └── manifest.json │ ├── edge │ │ └── manifest.json │ └── firefox │ │ └── manifest.json ├── stores │ └── settings.ts └── vite.d.ts ├── test └── common │ ├── expected │ ├── firefoxSeparator.html │ ├── folder.html │ ├── folderIncludeDate.html │ ├── includeDate.html │ ├── includeIcon.html │ ├── noOtherBookmarks.html │ ├── noParentFolders.html │ ├── other.html │ └── simple.html │ └── tools.spec.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | package.json text eol=lf 2 | /test/common/expected/*.html text eol=lf 3 | -------------------------------------------------------------------------------- /.gitea/scripts/tag-changelog-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { 4 | types: ['feat'], 5 | label: 'Features', 6 | }, 7 | { 8 | types: ['fix'], 9 | label: 'Bug Fixes', 10 | }, 11 | { 12 | types: ['perf'], 13 | label: 'Performance Improvements', 14 | }, 15 | { 16 | types: ['enh'], 17 | label: 'Enhancements', 18 | }, 19 | { 20 | types: ['tweak'], 21 | label: 'Refinements', 22 | }, 23 | { 24 | types: ['adjust'], 25 | label: 'Adjustments', 26 | }, 27 | { 28 | types: ['simplify'], 29 | label: 'Simplifications', 30 | }, 31 | { 32 | types: ['deprecate'], 33 | label: 'Deprecations', 34 | }, 35 | { 36 | types: ['ui'], 37 | label: 'UI Improvements', 38 | }, 39 | { 40 | types: ['security'], 41 | label: 'Security Fixes', 42 | }, 43 | ], 44 | excludeTypes: ['refactor', 'test', 'docs', 'typo', 'style', 'types', 'chore', 'config', 'build', 'ci', 'revert', 'init', 'merge'], 45 | }; 46 | -------------------------------------------------------------------------------- /.gitea/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | container: catthehacker/ubuntu:act-latest 12 | env: 13 | HTTP_PROXY: ${{ vars.PROXY }} 14 | HTTPS_PROXY: ${{ vars.PROXY }} 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | fetch-tags: true 21 | - name: Setup pnpm 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: 10 25 | - name: Setup Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 22 29 | cache: 'pnpm' 30 | - name: Build package 31 | run: | 32 | pnpm install 33 | pnpm run build 34 | - name: Get git tags 35 | id: git_tags 36 | run: | 37 | current=$(git describe --abbrev=0 --tags) 38 | echo "current=${current}" >> ${GITHUB_OUTPUT} 39 | prev=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`) 40 | echo "prev=${prev}" >> ${GITHUB_OUTPUT} 41 | - name: Create changelog text 42 | id: changelog_text 43 | uses: dragonish/tag-changelog@v1 44 | with: 45 | token: ${{ secrets.ACCESS_TOKEN }} 46 | config_file: ../.gitea/scripts/tag-changelog-config.cjs 47 | - name: Create release 48 | uses: akkuman/gitea-release-action@v1 49 | env: 50 | NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 51 | with: 52 | server_url: ${{ vars.SERVER }} 53 | files: |- 54 | archive/** 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | name: Release ${{ steps.git_tags.outputs.current }} 57 | body: | 58 | ${{ steps.changelog_text.outputs.changes }} 59 | 60 | --- 61 | 62 | ## Details 63 | 64 | See: [${{ steps.git_tags.outputs.prev }}...${{ steps.git_tags.outputs.current }}](/compare/${{ steps.git_tags.outputs.prev }}...${{ steps.git_tags.outputs.current }}) 65 | - name: Send notification 66 | if: ${{ !cancelled() && vars.CHAT_URL != '' }} 67 | uses: dragonish/send-to-synology-chat@v1 68 | with: 69 | webhook-url: ${{ vars.CHAT_URL }} 70 | message: "${{ gitea.repository }}\n\n${{ steps.git_tags.outputs.current }}\n\n#${{ job.status }}" 71 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: monogamy -------------------------------------------------------------------------------- /.github/scripts/tag-changelog-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { 4 | types: ['feat'], 5 | label: 'Features', 6 | }, 7 | { 8 | types: ['fix'], 9 | label: 'Bug Fixes', 10 | }, 11 | { 12 | types: ['perf'], 13 | label: 'Performance Improvements', 14 | }, 15 | { 16 | types: ['enh'], 17 | label: 'Enhancements', 18 | }, 19 | { 20 | types: ['tweak'], 21 | label: 'Refinements', 22 | }, 23 | { 24 | types: ['adjust'], 25 | label: 'Adjustments', 26 | }, 27 | { 28 | types: ['simplify'], 29 | label: 'Simplifications', 30 | }, 31 | { 32 | types: ['deprecate'], 33 | label: 'Deprecations', 34 | }, 35 | { 36 | types: ['ui'], 37 | label: 'UI Improvements', 38 | }, 39 | { 40 | types: ['security'], 41 | label: 'Security Fixes', 42 | }, 43 | ], 44 | excludeTypes: ['refactor', 'test', 'docs', 'typo', 'style', 'types', 'chore', 'config', 'build', 'ci', 'revert', 'init', 'merge'], 45 | }; 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | fetch-tags: true 17 | - name: Setup pnpm 18 | uses: pnpm/action-setup@v4 19 | with: 20 | version: 10 21 | - name: Setup Node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | cache: 'pnpm' 26 | - name: Build package 27 | run: | 28 | pnpm install 29 | pnpm run build 30 | - name: Create changelog text 31 | id: changelog_text 32 | uses: dragonish/tag-changelog@v1 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | config_file: .github/scripts/tag-changelog-config.cjs 36 | - name: Get git tags 37 | id: git_tags 38 | run: | 39 | current=$(git describe --abbrev=0 --tags) 40 | echo "current=${current}" >> ${GITHUB_OUTPUT} 41 | - name: Create release 42 | uses: ncipollo/release-action@v1 43 | with: 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | name: Release ${{ steps.git_tags.outputs.current }} 46 | prerelease: false 47 | draft: false 48 | artifacts: "archive/*.zip" 49 | body: ${{ steps.changelog_text.outputs.changes }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /build 5 | /archive 6 | 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.mocharc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ui: 'bdd', 3 | spec: ['test/**/**.spec.ts'], 4 | import: 'tsx', 5 | require: 'jsdom-global/register', 6 | }; 7 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bumpFiles": [ 3 | { 4 | "filename": "./package.json", 5 | "type": "json" 6 | }, 7 | { 8 | "filename": "./src/manifest/chrome/manifest.json", 9 | "type": "json" 10 | }, 11 | { 12 | "filename": "./src/manifest/firefox/manifest.json", 13 | "type": "json" 14 | }, 15 | { 16 | "filename": "./src/manifest/edge/manifest.json", 17 | "type": "json" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Light 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selective Bookmarks Export Tool 2 | 3 | [](https://github.com/LightAPIs/free-export-bookmarks/releases/latest) [](https://chrome.google.com/webstore/detail/selective-bookmarks-export-tool/dkbihgadoohejmlhpffffbmbhmkhjbfi) [](https://addons.mozilla.org/en-US/firefox/addon/bookmarks-export-tool/) [](https://microsoftedge.microsoft.com/addons/detail/eedggiamkopgoloilafiinldaablcohj) [](/LICENSE) 4 | 5 |
English | 简体中文
6 | 7 | > Freely bookmark export tool 8 | 9 | It allows users to choose the bookmarks they want to export as HTML file, to decide the data structure of the exported content, and to filter the results by keywords when selecting bookmarks. 10 | 11 | ## Installation 12 | 13 | ### Chrome 14 | 15 | Go to the [Chrome Web Store](https://chrome.google.com/webstore/detail/selective-bookmarks-export-tool/dkbihgadoohejmlhpffffbmbhmkhjbfi) to download and install. 16 | 17 | ### Firefox 18 | 19 | Go to the [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/bookmarks-export-tool/) to download and install. 20 | 21 | ### Edge 22 | 23 | Go to the [Microsoft Edge Addons](https://microsoftedge.microsoft.com/addons/detail/eedggiamkopgoloilafiinldaablcohj) to download and install. 24 | 25 | ## Development 26 | 27 | ### Environment 28 | 29 | - Install [Node.js](https://nodejs.org/) 20+ 30 | 31 | ### Initialization 32 | 33 | ```bash 34 | # Install pnpm 35 | npm install -g pnpm 36 | 37 | # Installation dependency 38 | pnpm install 39 | ``` 40 | 41 | ### Build 42 | 43 | - Build the Chrome version: `pnpm run build:c` 44 | - Build the Firefox version: `pnpm run build:f` 45 | - Build the Edge version: `pnpm run build:e` 46 | 47 | ### Related 48 | 49 | - Package configuration is located in `vite.config.ts` 50 | - Extension source code is in the `src` directory 51 | - Without changing the configuration, all files and folders in the `src/assets` directory will be automatically copied to the root directory when packaging 52 | 53 | ## Licence 54 | 55 | [MIT](/LICENSE) License 56 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Selective Bookmarks Export Tool 2 | 3 | [](https://github.com/LightAPIs/free-export-bookmarks/releases/latest) [](https://chrome.google.com/webstore/detail/selective-bookmarks-export-tool/dkbihgadoohejmlhpffffbmbhmkhjbfi) [](https://addons.mozilla.org/zh-CN/firefox/addon/bookmarks-export-tool/) [](https://microsoftedge.microsoft.com/addons/detail/eedggiamkopgoloilafiinldaablcohj) [](/LICENSE) 4 | 5 |English | 简体中文
6 | 7 | > 自由地书签导出工具 8 | 9 | 允许用户自定义选择导出所需要的书签为 HTML 文件,并且可以自行决定导出内容的数据结构,同时在选择书签时支持通过关键字进行结果过滤。 10 | 11 | ## 安装方法 12 | 13 | ### Chrome 14 | 15 | 前往 [chrome 网上应用店](https://chrome.google.com/webstore/detail/selective-bookmarks-export-tool/dkbihgadoohejmlhpffffbmbhmkhjbfi) 进行下载安装。 16 | 17 | ### Firefox 18 | 19 | 前往 [Mozilla Add-ons](https://addons.mozilla.org/zh-CN/firefox/addon/bookmarks-export-tool/) 进行下载安装。 20 | 21 | ### Edge 22 | 23 | 前往 [Microsoft Edge Addons](https://microsoftedge.microsoft.com/addons/detail/eedggiamkopgoloilafiinldaablcohj) 进行下载安装。 24 | 25 | ## 开发编译 26 | 27 | ### 环境需求 28 | 29 | - 安装 [Node.js](https://nodejs.org/) 20+ 30 | 31 | ### 初始化指令 32 | 33 | ```bash 34 | # 安装 pnpm 35 | npm install -g pnpm 36 | 37 | # 安装依赖 38 | pnpm install 39 | ``` 40 | 41 | ### 构建指令 42 | 43 | - 构建 chrome 版本: `npm run build:c` 44 | - 构建 firefox 版本: `npm run build:f` 45 | - 构建 edge 版本: `npm run build:e` 46 | 47 | ### 相关目录及文件 48 | 49 | - 与打包相关的配置位于 `vite.config.ts` 文件中 50 | - 扩展程序源代码位于 `src` 目录中 51 | - 未改动配置的情况下,`src/assets` 目录下所有文件及文件夹在打包时会自动复制到项目根目录 52 | 53 | ## 许可证 54 | 55 | [MIT](/LICENSE) License 56 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | // biome-ignore lint: disable 6 | export {} 7 | 8 | /* prettier-ignore */ 9 | declare module 'vue' { 10 | export interface GlobalComponents { 11 | BSettingsDrawer: typeof import('./src/components/BSettingsDrawer.vue')['default'] 12 | ElButton: typeof import('element-plus/es')['ElButton'] 13 | ElCollapse: typeof import('element-plus/es')['ElCollapse'] 14 | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] 15 | ElContainer: typeof import('element-plus/es')['ElContainer'] 16 | ElDivider: typeof import('element-plus/es')['ElDivider'] 17 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 18 | ElHeader: typeof import('element-plus/es')['ElHeader'] 19 | ElIcon: typeof import('element-plus/es')['ElIcon'] 20 | ElInput: typeof import('element-plus/es')['ElInput'] 21 | ElMain: typeof import('element-plus/es')['ElMain'] 22 | ElProgress: typeof import('element-plus/es')['ElProgress'] 23 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 24 | ElTree: typeof import('element-plus/es')['ElTree'] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import pluginVue from 'eslint-plugin-vue'; 5 | 6 | export default [ 7 | { 8 | languageOptions: { 9 | globals: { ...globals.browser, ...globals.node, ...globals.webextensions }, 10 | }, 11 | }, 12 | pluginJs.configs.recommended, 13 | ...tseslint.configs.recommended, 14 | ...pluginVue.configs['flat/essential'], 15 | { 16 | files: ['*.vue', '**/*.vue'], 17 | languageOptions: { 18 | parserOptions: { 19 | parser: '@typescript-eslint/parser', 20 | }, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-export-bookmarks", 3 | "description": "A extension project", 4 | "version": "1.6.0", 5 | "author": "dragonish${await traverse(item.children, settings, space, isNoOther, progressHandle)} 136 | ${space}
`; 137 | } 138 | } else { 139 | //! for Firefox 140 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 141 | if ((item as any).type === 'separator') { 142 | html += ` 143 | ${settings.noParentFolders ? (isNoOther ? ' ' : ' ') : space}
${await traverse(arr, settings, '', false, progressHandle)} 175 |
176 | `;
177 | if (typeof progressCompleted === 'function') {
178 | progressCompleted();
179 | }
180 | return header + body;
181 | }
182 |
--------------------------------------------------------------------------------
/src/components/BSettingsDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ i18n('indexDrawerDisplayAutoExpandAllTip') }} {{ i18n('indexDrawerExportIncludeIconTip') }} {{ isChromium ? i18n('indexDrawerExportNoOtherBookmarksTip') : i18n('indexDrawerExportNoOtherBookmarksTipFirefox') }} {{ isChromium ? i18n('indexDrawerExportNoParentFoldersTip') : i18n('indexDrawerExportNoParentFoldersTipFirefox') }} {{ i18n('indexDrawerExportSaveAsTip') }} {{ i18n('indexDrawerAboutSupportLabel') }}
82 | {{ i18n('extensionName') + ' v' + version }}
83 |
92 |
96 |
37 |
9 |
11 |
13 |
15 |
19 |
20 | -------------------------------------------------------------------------------- /test/common/expected/folder.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
14 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /test/common/expected/folderIncludeDate.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
14 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /test/common/expected/includeDate.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
13 |
14 | -------------------------------------------------------------------------------- /test/common/expected/includeIcon.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
13 |
14 | -------------------------------------------------------------------------------- /test/common/expected/noOtherBookmarks.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
14 |
16 |
17 |
19 | -------------------------------------------------------------------------------- /test/common/expected/noParentFolders.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
14 |
15 | -------------------------------------------------------------------------------- /test/common/expected/other.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
14 |
16 |
17 |
19 |
21 |
22 | -------------------------------------------------------------------------------- /test/common/expected/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
9 |
11 |
13 |
14 | -------------------------------------------------------------------------------- /test/common/tools.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import { expect } from 'chai'; 5 | import { htmlFileGenerator } from '@/common/tools'; 6 | 7 | type firefoxBookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode & { type: 'bookmark' | 'folder' | 'separator'; children?: firefoxBookmarkTreeNode[] }; 8 | 9 | function readFile(htmlName: string) { 10 | const __dirname = import.meta.dirname; 11 | return fs.readFileSync(path.resolve(__dirname, `./expected/${htmlName}.html`), 'utf-8'); 12 | } 13 | 14 | describe('tools/htmlFileGenerator', function () { 15 | const simpleNode: chrome.bookmarks.BookmarkTreeNode[] = [ 16 | { 17 | id: '1', 18 | index: 0, 19 | parentId: '0', 20 | title: 'Bookmarks bar', 21 | dateAdded: 1543251918305, 22 | dateGroupModified: 1715253428187, 23 | children: [ 24 | { 25 | id: '10001', 26 | parentId: '1', 27 | index: 0, 28 | dateAdded: 1662359122122, 29 | title: 'Google', 30 | url: 'https://www.google.com/', 31 | }, 32 | ], 33 | }, 34 | ]; 35 | 36 | it('simple', async function () { 37 | const res = await htmlFileGenerator(simpleNode, {}); 38 | const expected = readFile('simple'); 39 | expect(res).to.be.eq(expected); 40 | }); 41 | 42 | it('simple with includeIcon', async function () { 43 | const res = await htmlFileGenerator(simpleNode, { 44 | includeIcon: true, 45 | }); 46 | const expected = readFile('includeIcon'); 47 | expect(res).to.be.eq(expected); 48 | }); 49 | 50 | it('simple with includeDate', async function () { 51 | const res = await htmlFileGenerator(simpleNode, { 52 | includeDate: true, 53 | }); 54 | const expected = readFile('includeDate'); 55 | expect(res).to.be.eq(expected); 56 | }); 57 | 58 | const folderNode: chrome.bookmarks.BookmarkTreeNode[] = [ 59 | { 60 | id: '1', 61 | index: 0, 62 | parentId: '0', 63 | title: 'Bookmarks bar', 64 | dateAdded: 1543251918305, 65 | dateGroupModified: 1715253428187, 66 | children: [ 67 | { 68 | id: '10001', 69 | parentId: '1', 70 | index: 0, 71 | dateAdded: 1662359122122, 72 | title: 'Google', 73 | url: 'https://www.google.com/', 74 | }, 75 | { 76 | id: '10002', 77 | parentId: '1', 78 | index: 1, 79 | title: 'Folder', 80 | dateAdded: 1662359121159, 81 | dateGroupModified: 1662359122124, 82 | children: [ 83 | { 84 | id: '10003', 85 | parentId: '10002', 86 | index: 0, 87 | dateAdded: 1718018419639, 88 | title: 'GitHub', 89 | url: 'https://github.com/', 90 | }, 91 | ], 92 | }, 93 | ], 94 | }, 95 | ]; 96 | 97 | it('folder', async function () { 98 | const res = await htmlFileGenerator(folderNode, {}); 99 | const expected = readFile('folder'); 100 | expect(res).to.be.eq(expected); 101 | }); 102 | 103 | it('folder with includeDate', async function () { 104 | const res = await htmlFileGenerator(folderNode, { 105 | includeDate: true, 106 | }); 107 | const expected = readFile('folderIncludeDate'); 108 | expect(res).to.be.eq(expected); 109 | }); 110 | 111 | it('folder with noParentFolders', async function () { 112 | const res = await htmlFileGenerator(folderNode, { 113 | noParentFolders: true, 114 | }); 115 | const expected = readFile('noParentFolders'); 116 | expect(res).to.be.eq(expected); 117 | }); 118 | 119 | const otherNode: chrome.bookmarks.BookmarkTreeNode[] = [ 120 | { 121 | id: '1', 122 | index: 0, 123 | parentId: '0', 124 | title: 'Bookmarks bar', 125 | dateAdded: 1543251918305, 126 | dateGroupModified: 1715253428187, 127 | children: [ 128 | { 129 | id: '10001', 130 | parentId: '1', 131 | index: 0, 132 | dateAdded: 1662359122122, 133 | title: 'Google', 134 | url: 'https://www.google.com/', 135 | }, 136 | { 137 | id: '10002', 138 | parentId: '1', 139 | index: 1, 140 | title: 'Folder', 141 | dateAdded: 1662359121159, 142 | dateGroupModified: 1662359122124, 143 | children: [ 144 | { 145 | id: '10003', 146 | parentId: '10002', 147 | index: 0, 148 | dateAdded: 1718018419639, 149 | title: 'GitHub', 150 | url: 'https://github.com/', 151 | }, 152 | ], 153 | }, 154 | ], 155 | }, 156 | { 157 | id: '2', 158 | index: 1, 159 | dateAdded: 1543251918305, 160 | dateGroupModified: 1682310070383, 161 | parentId: '0', 162 | title: 'Other bookmarks', 163 | children: [ 164 | { 165 | id: '11001', 166 | parentId: '2', 167 | index: 0, 168 | dateAdded: 1718019712232, 169 | title: 'Home / X', 170 | url: 'https://x.com/home', 171 | }, 172 | ], 173 | }, 174 | ]; 175 | 176 | it('other', async function () { 177 | const res = await htmlFileGenerator(otherNode, {}); 178 | const expected = readFile('other'); 179 | expect(res).to.be.eq(expected); 180 | }); 181 | 182 | it('other with noOtherBookmarks', async function () { 183 | const res = await htmlFileGenerator(otherNode, { 184 | noOtherBookmarks: true, 185 | }); 186 | const expected = readFile('noOtherBookmarks'); 187 | expect(res).to.be.eq(expected); 188 | }); 189 | 190 | const firefoxNode: firefoxBookmarkTreeNode[] = [ 191 | { 192 | id: 'menu________', 193 | title: 'Bookmarks Toolbar', 194 | dateAdded: 1700284756702, 195 | dateGroupModified: 1700284756794, 196 | index: 0, 197 | parentId: 'root________', 198 | type: 'folder', 199 | children: [ 200 | { 201 | id: 'cojpCOyb0lMS', 202 | parentId: 'menu________', 203 | index: 0, 204 | dateAdded: 1700284756794, 205 | title: 'About', 206 | url: 'https://www.mozilla.org/about/', 207 | type: 'bookmark', 208 | }, 209 | ], 210 | }, 211 | { 212 | id: 'toolbar_____', 213 | title: 'Bookmarks Menu', 214 | dateAdded: 1700284756702, 215 | dateGroupModified: 1700284756794, 216 | index: 1, 217 | parentId: 'root________', 218 | type: 'folder', 219 | children: [ 220 | { 221 | id: 'oztVbdoL7i-c', 222 | parentId: 'toolbar_____', 223 | index: 0, 224 | dateAdded: 1723884832518, 225 | title: 'Google', 226 | url: 'https://www.google.com/', 227 | type: 'bookmark', 228 | }, 229 | { 230 | id: 'ikoBYA5wHzXP', 231 | parentId: 'toolbar_____', 232 | index: 1, 233 | dateAdded: 1723896319613, 234 | title: '', 235 | url: 'data:', 236 | type: 'separator', 237 | }, 238 | { 239 | id: 'djobiKNRfCZh', 240 | parentId: 'toolbar_____', 241 | index: 2, 242 | dateAdded: 1700284756802, 243 | title: 'GitHub', 244 | url: 'https://github.com/', 245 | type: 'bookmark', 246 | }, 247 | ], 248 | }, 249 | ]; 250 | 251 | it('firefox separator', async function () { 252 | const res = await htmlFileGenerator(firefoxNode, { includeDate: true }); 253 | const expected = readFile('firefoxSeparator'); 254 | expect(res).to.be.eq(expected); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["src/*"] 25 | }, 26 | "types": ["node", "chrome", "firefox-webext-browser", "./components.d.ts"] 27 | }, 28 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "test/**/*.ts"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import path from 'node:path'; 4 | import packageInfo from './package.json'; 5 | import HTMLLocation from 'rollup-plugin-html-location'; 6 | import AutoImport from 'unplugin-auto-import/vite'; 7 | import Components from 'unplugin-vue-components/vite'; 8 | import ElementPlus from 'unplugin-element-plus/vite'; 9 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 10 | import Copy from 'rollup-plugin-copy'; 11 | import ZipPack from 'vite-plugin-zip-pack'; 12 | 13 | const productionMode = process.env.NODE_ENV === 'production'; 14 | const modeDir = productionMode ? 'build' : 'dist'; 15 | const browserName = process.env.BROWSER_ENV || 'unknown'; 16 | const isChromium = browserName === 'chrome' || browserName === 'edge'; 17 | 18 | const htmlNames = ['index']; 19 | const jsNames = ['background']; 20 | const pages = {}; 21 | htmlNames.forEach(name => { 22 | pages[name] = path.resolve(__dirname, `src/${name}/index.html`); 23 | }); 24 | jsNames.forEach(name => { 25 | pages[name] = path.resolve(__dirname, `src/${name}/index.ts`); 26 | }); 27 | const outDir = `${path.resolve(__dirname, modeDir, browserName)}`; 28 | 29 | // https://vitejs.dev/config/ 30 | export default defineConfig({ 31 | build: { 32 | outDir, 33 | sourcemap: !productionMode, 34 | chunkSizeWarningLimit: 1024, 35 | rollupOptions: { 36 | input: pages, 37 | output: { 38 | entryFileNames: '[name]/index.js', 39 | }, 40 | }, 41 | }, 42 | resolve: { 43 | alias: { 44 | '@': path.resolve(__dirname, 'src'), 45 | }, 46 | }, 47 | publicDir: 'src/assets', 48 | plugins: [ 49 | vue(), 50 | AutoImport({ 51 | resolvers: [ElementPlusResolver()], 52 | }), 53 | Components({ 54 | resolvers: [ElementPlusResolver()], 55 | }), 56 | ElementPlus({}), 57 | Copy({ 58 | targets: [ 59 | { 60 | src: `src/manifest/${browserName}/manifest.json`, 61 | dest: outDir, 62 | }, 63 | ], 64 | hook: 'writeBundle', 65 | }), 66 | HTMLLocation({ 67 | filename: input => input.replace('src/', ''), 68 | }), 69 | productionMode 70 | ? ZipPack({ 71 | inDir: outDir, 72 | outDir: 'archive', 73 | outFileName: `selective-bookmarks-export-tool_${browserName}_v${packageInfo.version}.zip`, 74 | }) 75 | : undefined, 76 | ], 77 | define: { 78 | 'import.meta.env.BROWSER': JSON.stringify(browserName), 79 | 'import.meta.env.IS_CHROMIUM': isChromium, 80 | 'import.meta.env.VERSION': JSON.stringify(packageInfo.version), 81 | }, 82 | }); 83 | --------------------------------------------------------------------------------