├── .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 | [![Release](https://img.shields.io/github/v/release/LightAPIs/free-export-bookmarks.svg?color=orange)](https://github.com/LightAPIs/free-export-bookmarks/releases/latest) [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/dkbihgadoohejmlhpffffbmbhmkhjbfi?maxAge=86400)](https://chrome.google.com/webstore/detail/selective-bookmarks-export-tool/dkbihgadoohejmlhpffffbmbhmkhjbfi) [![Mozilla Add-ons](https://img.shields.io/amo/v/bookmarks-export-tool)](https://addons.mozilla.org/en-US/firefox/addon/bookmarks-export-tool/) [![Microsoft Edge Addons](https://img.shields.io/badge/-edge_addons-blue.svg)](https://microsoftedge.microsoft.com/addons/detail/eedggiamkopgoloilafiinldaablcohj) [![MIT](https://img.shields.io/badge/license-MIT-green)](/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 | [![Release](https://img.shields.io/github/v/release/LightAPIs/free-export-bookmarks.svg?color=orange)](https://github.com/LightAPIs/free-export-bookmarks/releases/latest) [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/dkbihgadoohejmlhpffffbmbhmkhjbfi?maxAge=86400)](https://chrome.google.com/webstore/detail/selective-bookmarks-export-tool/dkbihgadoohejmlhpffffbmbhmkhjbfi) [![Mozilla Add-ons](https://img.shields.io/amo/v/bookmarks-export-tool)](https://addons.mozilla.org/zh-CN/firefox/addon/bookmarks-export-tool/) [![Microsoft Edge Addons](https://img.shields.io/badge/-edge_addons-blue.svg)](https://microsoftedge.microsoft.com/addons/detail/eedggiamkopgoloilafiinldaablcohj) [![MIT](https://img.shields.io/badge/license-MIT-green)](/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 ", 6 | "license": "MIT", 7 | "type": "module", 8 | "private": true, 9 | "scripts": { 10 | "watch:c": "cross-env NODE_ENV=development BROWSER_ENV=chrome vite build --watch", 11 | "watch:f": "cross-env NODE_ENV=development BROWSER_ENV=firefox vite build --watch", 12 | "watch:e": "cross-env NODE_ENV=development BROWSER_ENV=edge vite build --watch", 13 | "build:c": "cross-env BROWSER_ENV=chrome vite build", 14 | "build:f": "cross-env BROWSER_ENV=firefox vite build", 15 | "build:e": "cross-env BROWSER_ENV=edge vite build", 16 | "build": "pnpm run build:c && pnpm run build:f && pnpm run build:e", 17 | "release": "standard-version", 18 | "test": "vue-tsc && cross-env NODE_NO_WARNINGS=1 mocha" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.28.0", 22 | "@types/chai": "^5.2.2", 23 | "@types/chrome": "^0.0.307", 24 | "@types/firefox-webext-browser": "^120.0.4", 25 | "@types/lodash-es": "^4.17.12", 26 | "@types/mocha": "^10.0.10", 27 | "@types/node": "^22.15.29", 28 | "@typescript-eslint/parser": "^8.33.1", 29 | "@vitejs/plugin-vue": "^5.2.4", 30 | "chai": "^5.2.0", 31 | "cross-env": "^7.0.3", 32 | "eslint": "^9.28.0", 33 | "eslint-plugin-vue": "^10.1.0", 34 | "globals": "^16.2.0", 35 | "jsdom": "^26.1.0", 36 | "jsdom-global": "^3.0.2", 37 | "mocha": "^11.5.0", 38 | "rollup-plugin-copy": "^3.5.0", 39 | "rollup-plugin-html-location": "^0.1.2", 40 | "standard-version": "^9.5.0", 41 | "tsx": "^4.19.4", 42 | "typescript": "^5.8.3", 43 | "typescript-eslint": "^8.33.1", 44 | "unplugin-auto-import": "^19.3.0", 45 | "unplugin-element-plus": "^0.10.0", 46 | "unplugin-vue-components": "^28.7.0", 47 | "vite": "^6.3.5", 48 | "vite-plugin-zip-pack": "^1.2.4", 49 | "vue-eslint-parser": "^10.1.3", 50 | "vue-tsc": "^2.2.10" 51 | }, 52 | "standard-version": { 53 | "skip": { 54 | "changelog": true 55 | }, 56 | "scripts": { 57 | "posttag": "pnpm run build" 58 | } 59 | }, 60 | "dependencies": { 61 | "@element-plus/icons-vue": "^2.3.1", 62 | "@vueuse/core": "^13.3.0", 63 | "element-plus": "^2.9.11", 64 | "lodash-es": "^4.17.21", 65 | "pinia": "^3.0.2", 66 | "vue": "^3.5.16" 67 | }, 68 | "pnpm": { 69 | "onlyBuiltDependencies": [ 70 | "esbuild", 71 | "vue-demi" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/assets/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Selective Bookmarks Export Tool" 4 | }, 5 | "extensionDescription": { 6 | "message": "Freely export the required bookmarks as HTML file" 7 | }, 8 | "extensionTip": { 9 | "message": "Open the export bookmarks page" 10 | }, 11 | "indexTitle": { 12 | "message": "Export bookmarks" 13 | }, 14 | "indexExportText": { 15 | "message": "Export" 16 | }, 17 | "indexExportLoadingText": { 18 | "message": "Exporting" 19 | }, 20 | "indexExportTip": { 21 | "message": "Export selected bookmarks as HTML file" 22 | }, 23 | "indexReloadText": { 24 | "message": "Reload" 25 | }, 26 | "indexReloadTip": { 27 | "message": "Reload bookmark list" 28 | }, 29 | "indexInputPlaceholder": { 30 | "message": "Enter keywords to filter" 31 | }, 32 | "indexExportNoSelectedTip": { 33 | "message": "Please select the bookmarks to be exported" 34 | }, 35 | "indexExportSuccessTip": { 36 | "message": "Bookmark export completed" 37 | }, 38 | "indexLoadingText": { 39 | "message": "Desperately loading" 40 | }, 41 | "indexDrawerTitle": { 42 | "message": "More options" 43 | }, 44 | "indexDrawerDisplayTitle": { 45 | "message": "Display content" 46 | }, 47 | "indexDrawerDisplayShowIconText": { 48 | "message": "Show bookmark icon" 49 | }, 50 | "indexDrawerDisplayAutoExpandAllText": { 51 | "message": "Automatically expand all folders when bookmarks are first loaded" 52 | }, 53 | "indexDrawerDisplayAutoExpandAllTip": { 54 | "message": "Note: When this option is enabled, if there are too many bookmarks, performance will be affected." 55 | }, 56 | "indexDrawerExportTitle": { 57 | "message": "Export content" 58 | }, 59 | "indexDrawerExportIncludeIconText": { 60 | "message": "Include website icon data in the exported file" 61 | }, 62 | "indexDrawerExportIncludeIconTip": { 63 | "message": "Note: This will significantly increase the time required to export the file and the file size." 64 | }, 65 | "indexDrawerExportIncludeDateText": { 66 | "message": "Include creation date and modification date data in the exported file" 67 | }, 68 | "indexDrawerExportNoOtherBookmarksText": { 69 | "message": "The root directory of \"Other bookmarks\" is not displayed in the exported file" 70 | }, 71 | "indexDrawerExportNoOtherBookmarksTextFirefox": { 72 | "message": "The root directory such as \"Other Bookmarks\" will not be displayed in the exported file" 73 | }, 74 | "indexDrawerExportNoOtherBookmarksTip": { 75 | "message": "This is the browser's native way of exporting bookmarks, meaning that the content under \"Other bookmarks\" in the exported result is at the same level as the \"Bookmarks bar\"." 76 | }, 77 | "indexDrawerExportNoOtherBookmarksTipFirefox": { 78 | "message": "This is the browser's native way of exporting bookmarks, meaning that the content under \"Other Bookmarks\" in the exported result is at the same level as the \"Bookmarks Menu\"." 79 | }, 80 | "indexDrawerExportNoParentFoldersText": { 81 | "message": "The parent folder is not displayed in the exported file" 82 | }, 83 | "indexDrawerExportNoParentFoldersTip": { 84 | "message": "In the export result, all bookmarks are directly located under the root directories such as \"Bookmarks bar\" and \"Other bookmarks\"." 85 | }, 86 | "indexDrawerExportNoParentFoldersTipFirefox": { 87 | "message": "In the export result, all bookmarks are directly located under the root directories such as \"Bookmarks Menu\" and \"Other Bookmarks\"." 88 | }, 89 | "indexDrawerExportSaveAsText": { 90 | "message": "Show file save as window" 91 | }, 92 | "indexDrawerExportSaveAsTip": { 93 | "message": "To support custom save location for exported file." 94 | }, 95 | "indexDrawerAboutTitle": { 96 | "message": "More information" 97 | }, 98 | "indexDrawerAboutSupportText": { 99 | "message": "Support" 100 | }, 101 | "indexDrawerAboutSupportLabel": { 102 | "message": "Selective Bookmarks Export Tool is a free and open source bookmarks export tool, which helps users to export part of their bookmarks for saving or sharing, and never collect any private information from users." 103 | }, 104 | "indexDrawerAboutSourceCodeText": { 105 | "message": "Source code (MIT Liscense)" 106 | }, 107 | "indexDrawerAboutFeedbackText": { 108 | "message": "Feedback" 109 | }, 110 | "contextMenuExport": { 111 | "message": "Export current bookmark" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/assets/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Selective Bookmarks Export Tool" 4 | }, 5 | "extensionDescription": { 6 | "message": "自由导出所需要的书签为 HTML 文件" 7 | }, 8 | "extensionTip": { 9 | "message": "点击打开导出书签页面" 10 | }, 11 | "indexTitle": { 12 | "message": "导出书签" 13 | }, 14 | "indexExportText": { 15 | "message": "导出" 16 | }, 17 | "indexExportLoadingText": { 18 | "message": "导出中" 19 | }, 20 | "indexExportTip": { 21 | "message": "导出已选择的书签为 HTML 文件" 22 | }, 23 | "indexReloadText": { 24 | "message": "重新加载" 25 | }, 26 | "indexReloadTip": { 27 | "message": "重新加载书签列表" 28 | }, 29 | "indexInputPlaceholder": { 30 | "message": "输入关键字进行过滤" 31 | }, 32 | "indexExportNoSelectedTip": { 33 | "message": "请选择需要导出的书签" 34 | }, 35 | "indexExportSuccessTip": { 36 | "message": "已完成书签导出" 37 | }, 38 | "indexLoadingText": { 39 | "message": "拼命加载中" 40 | }, 41 | "indexDrawerTitle": { 42 | "message": "更多选项" 43 | }, 44 | "indexDrawerDisplayTitle": { 45 | "message": "展示内容" 46 | }, 47 | "indexDrawerDisplayShowIconText": { 48 | "message": "显示书签的图标" 49 | }, 50 | "indexDrawerDisplayAutoExpandAllText": { 51 | "message": "在首次加载书签时自动展开所有文件夹" 52 | }, 53 | "indexDrawerDisplayAutoExpandAllTip": { 54 | "message": "注意:在启用该选项时,若书签数量过多,性能将会受到影响。" 55 | }, 56 | "indexDrawerExportTitle": { 57 | "message": "导出内容" 58 | }, 59 | "indexDrawerExportIncludeIconText": { 60 | "message": "在导出的文件中包含网站图标数据" 61 | }, 62 | "indexDrawerExportIncludeIconTip": { 63 | "message": "注意:这将显著增加导出文件所需时间和文件大小。" 64 | }, 65 | "indexDrawerExportIncludeDateText": { 66 | "message": "在导出的文件中包含创建日期和修改日期数据" 67 | }, 68 | "indexDrawerExportNoOtherBookmarksText": { 69 | "message": "在导出的文件中不显示\"其他书签\"根目录" 70 | }, 71 | "indexDrawerExportNoOtherBookmarksTextFirefox": { 72 | "message": "在导出的文件中不显示\"其他书签\"等根目录" 73 | }, 74 | "indexDrawerExportNoOtherBookmarksTip": { 75 | "message": "这是浏览器原生导出书签功能的处理方式,即在导出结果中\"其他书签\"下的内容与\"书签栏\"为同一层级。" 76 | }, 77 | "indexDrawerExportNoOtherBookmarksTipFirefox": { 78 | "message": "即在导出结果中\"其他书签\"等目录下的内容与\"书签菜单\"为同一层级。" 79 | }, 80 | "indexDrawerExportNoParentFoldersText": { 81 | "message": "在导出的文件中不显示父级文件夹" 82 | }, 83 | "indexDrawerExportNoParentFoldersTip": { 84 | "message": "即在导出结果中所有书签都直接位于\"书签栏\"和\"其他书签\"等根目录下。" 85 | }, 86 | "indexDrawerExportNoParentFoldersTipFirefox": { 87 | "message": "即在导出结果中所有书签都直接位于\"书签菜单\"和\"其他书签\"等根目录下。" 88 | }, 89 | "indexDrawerExportSaveAsText": { 90 | "message": "显示文件另存为窗口" 91 | }, 92 | "indexDrawerExportSaveAsTip": { 93 | "message": "以支持自定义导出文件的保存位置。" 94 | }, 95 | "indexDrawerAboutTitle": { 96 | "message": "更多信息" 97 | }, 98 | "indexDrawerAboutSupportText": { 99 | "message": "支持" 100 | }, 101 | "indexDrawerAboutSupportLabel": { 102 | "message": "Selective Bookmarks Export Tool 是一款免费开源的书签导出工具,皆在帮助用户更方便导出所需的部分书签以供保存或分享,并且绝不会收集发送用户的任何隐私信息。" 103 | }, 104 | "indexDrawerAboutSourceCodeText": { 105 | "message": "源代码 (MIT Liscense)" 106 | }, 107 | "indexDrawerAboutFeedbackText": { 108 | "message": "问题反馈" 109 | }, 110 | "contextMenuExport": { 111 | "message": "导出当前书签" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/assets/_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Selective Bookmarks Export Tool" 4 | }, 5 | "extensionDescription": { 6 | "message": "自由匯出所需要的書籤為 HTML 檔案" 7 | }, 8 | "extensionTip": { 9 | "message": "點擊打開匯出書籤頁面" 10 | }, 11 | "indexTitle": { 12 | "message": "匯出書籤" 13 | }, 14 | "indexExportText": { 15 | "message": "匯出" 16 | }, 17 | "indexExportLoadingText": { 18 | "message": "匯出中" 19 | }, 20 | "indexExportTip": { 21 | "message": "匯出已選擇的書籤為 HTML 檔案" 22 | }, 23 | "indexReloadText": { 24 | "message": "重新載入" 25 | }, 26 | "indexReloadTip": { 27 | "message": "重新載入書籤列表" 28 | }, 29 | "indexInputPlaceholder": { 30 | "message": "輸入關鍵字進行過濾" 31 | }, 32 | "indexExportNoSelectedTip": { 33 | "message": "請選擇需要匯出的書籤" 34 | }, 35 | "indexExportSuccessTip": { 36 | "message": "已完成書籤匯出" 37 | }, 38 | "indexLoadingText": { 39 | "message": "拚命載入中" 40 | }, 41 | "indexDrawerTitle": { 42 | "message": "更多選項" 43 | }, 44 | "indexDrawerDisplayTitle": { 45 | "message": "顯示內容" 46 | }, 47 | "indexDrawerDisplayShowIconText": { 48 | "message": "顯示書籤的圖示" 49 | }, 50 | "indexDrawerDisplayAutoExpandAllText": { 51 | "message": "在首次載入書籤時自動展開所有資料夾" 52 | }, 53 | "indexDrawerDisplayAutoExpandAllTip": { 54 | "message": "注意:在啟用該選項時,若書籤數量過多,性能將會受到影響。" 55 | }, 56 | "indexDrawerExportTitle": { 57 | "message": "匯出內容" 58 | }, 59 | "indexDrawerExportIncludeIconText": { 60 | "message": "在匯出的檔案中包含網站圖示數據" 61 | }, 62 | "indexDrawerExportIncludeIconTip": { 63 | "message": "注意:這將顯著增加匯出檔案所需時間和檔案大小。" 64 | }, 65 | "indexDrawerExportIncludeDateText": { 66 | "message": "在匯出的檔案中包含創建日期和修改日期數據" 67 | }, 68 | "indexDrawerExportNoOtherBookmarksText": { 69 | "message": "在匯出的檔案中不顯示\"其他書籤\"根目錄" 70 | }, 71 | "indexDrawerExportNoOtherBookmarksTextFirefox": { 72 | "message": "在匯出的檔案中不顯示\"其他書籤\"等根目錄" 73 | }, 74 | "indexDrawerExportNoOtherBookmarksTip": { 75 | "message": "這是瀏覽器原生匯出書籤功能的處理方式,即在匯出結果中\"其他書籤\"下的內容與\"書籤列\"為同一層級。" 76 | }, 77 | "indexDrawerExportNoOtherBookmarksTipFirefox": { 78 | "message": "即在匯出結果中\"其他書籤\"等目錄下的內容與\"書籤選單\"為同一層級。" 79 | }, 80 | "indexDrawerExportNoParentFoldersText": { 81 | "message": "在匯出的檔案中不顯示父級資料夾" 82 | }, 83 | "indexDrawerExportNoParentFoldersTip": { 84 | "message": "即在匯出結果中所有書籤都直接位於\"書籤列\"和\"其他書籤\"等根目錄下。" 85 | }, 86 | "indexDrawerExportNoParentFoldersTipFirefox": { 87 | "message": "即在匯出結果中所有書籤都直接位於\"書籤選單\"和\"其他書籤\"等根目錄下。" 88 | }, 89 | "indexDrawerExportSaveAsText": { 90 | "message": "顯示檔案另存為視窗" 91 | }, 92 | "indexDrawerExportSaveAsTip": { 93 | "message": "以支援自定義匯出檔案的儲存位置。" 94 | }, 95 | "indexDrawerAboutTitle": { 96 | "message": "更多資訊" 97 | }, 98 | "indexDrawerAboutSupportText": { 99 | "message": "支持" 100 | }, 101 | "indexDrawerAboutSupportLabel": { 102 | "message": "Selective Bookmarks Export Tool 是一款免費開源的書籤滙出工具,皆在幫助使用者更方便匯出所需的部分書籤以供儲存或分享,並且絕不會收集發送使用者的任何隱私資訊。" 103 | }, 104 | "indexDrawerAboutSourceCodeText": { 105 | "message": "源代碼 (MIT Liscense)" 106 | }, 107 | "indexDrawerAboutFeedbackText": { 108 | "message": "問題反饋" 109 | }, 110 | "contextMenuExport": { 111 | "message": "匯出當前書籤" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/assets/icons/app/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-128.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-16.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-19.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-24.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-32.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-48.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-64.png -------------------------------------------------------------------------------- /src/assets/icons/app/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/assets/icons/app/icon-96.png -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { downloadTextFile, htmlFileGenerator } from '@/common/tools'; 2 | 3 | chrome.action.onClicked.addListener(() => { 4 | chrome.tabs.create({ 5 | url: './index/index.html', 6 | }); 7 | }); 8 | 9 | if (import.meta.env.BROWSER === 'firefox') { 10 | browser.runtime.onInstalled.addListener(() => { 11 | browser.menus.create({ 12 | id: 'bookmarksMenu', 13 | type: 'normal', 14 | contexts: ['bookmark'], 15 | title: chrome.i18n.getMessage('contextMenuExport'), 16 | }); 17 | }); 18 | 19 | browser.menus.onClicked.addListener(info => { 20 | const { bookmarkId } = info; 21 | if (bookmarkId) { 22 | chrome.storage.local.get('settings', res => { 23 | const settings = res.settings || {}; 24 | const time = new Date(); 25 | chrome.bookmarks.getSubTree(bookmarkId, async results => { 26 | downloadTextFile( 27 | await htmlFileGenerator(results, settings), 28 | `bookmarks_${time.getFullYear().toString()}_${(time.getMonth() + 1).toString()}_${time.getDate().toString()}.html`, 29 | settings.saveAs 30 | ); 31 | }); 32 | }); 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/common/i18n.ts: -------------------------------------------------------------------------------- 1 | import messages from '@/assets/_locales/zh_CN/messages.json'; 2 | 3 | type messagesKey = keyof typeof messages; 4 | 5 | function i18n(messageName: messagesKey, substitutions?: string | string[] | undefined): string { 6 | return chrome.i18n.getMessage(messageName, substitutions); 7 | } 8 | 9 | export default i18n; 10 | -------------------------------------------------------------------------------- /src/common/tools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Download text file. 3 | * @param content file content 4 | * @param filename filename 5 | * @param saveAs save as 6 | * @param completed callback function 7 | */ 8 | export function downloadTextFile(content: string, filename: string, saveAs?: boolean, completed?: () => void) { 9 | if (!filename.endsWith('.html')) { 10 | filename += '.html'; 11 | } 12 | 13 | const exportBlob = new Blob([content], { type: 'text/html' }); 14 | const downloadUrl = window.URL.createObjectURL(exportBlob); 15 | 16 | chrome.downloads.download( 17 | { 18 | url: downloadUrl, 19 | filename, 20 | saveAs, 21 | }, 22 | () => { 23 | if (typeof completed === 'function') { 24 | completed(); 25 | } 26 | } 27 | ); 28 | } 29 | 30 | /** 31 | * Escape HTML characters. 32 | * @see https://www.cnblogs.com/daysme/p/7100553.html 33 | * @param html HTML content 34 | * @returns 35 | */ 36 | function htmlEncode(html: string) { 37 | let temp: HTMLDivElement | null = document.createElement('div'); 38 | temp.textContent = html; 39 | const output = temp.innerHTML; 40 | temp = null; 41 | return output; 42 | } 43 | 44 | /** 45 | * Get image base64 code. 46 | * @see https://blog.csdn.net/DeMonliuhui/article/details/79731359 47 | * @param src image address 48 | * @param mime image MIME type 49 | * @returns 50 | */ 51 | function getImageBase64(src: string, mime: string): Promise { 52 | try { 53 | let canvas: HTMLCanvasElement | null = document.createElement('canvas'); 54 | const ctx = canvas.getContext('2d'); 55 | const img = new Image(); 56 | img.crossOrigin = 'Anonymous'; 57 | img.src = src; 58 | return new Promise((resolve, reject) => { 59 | img.onload = () => { 60 | if (canvas && ctx) { 61 | canvas.width = 16; 62 | canvas.height = 16; 63 | ctx.drawImage(img, 0, 0, 16, 16); 64 | const dataURL = canvas.toDataURL('image/' + mime); 65 | resolve(dataURL); 66 | } else { 67 | const err = new Error('no canvas'); 68 | console.warn(err); 69 | reject(err); 70 | } 71 | canvas = null; 72 | }; 73 | img.onerror = err => { 74 | console.error(err); 75 | reject(err); 76 | }; 77 | }); 78 | } catch (err) { 79 | console.error(err); 80 | return Promise.reject(err); 81 | } 82 | } 83 | 84 | /** 85 | * Get link's icon. 86 | * @param url 87 | */ 88 | async function getIcon(url?: string) { 89 | if (import.meta.env == undefined) { 90 | // Skip in the automatic test environment 91 | return ''; 92 | } 93 | 94 | if (url) { 95 | return (await getImageBase64(faviconURL(url), 'png')) || ''; 96 | } 97 | return ''; 98 | } 99 | 100 | export function faviconURL(u: string) { 101 | const url = new URL(chrome.runtime.getURL('/_favicon/')); 102 | url.searchParams.set('pageUrl', u); 103 | url.searchParams.set('size', '16'); 104 | return url.toString(); 105 | } 106 | 107 | async function traverse( 108 | arr: chrome.bookmarks.BookmarkTreeNode[], 109 | settings: Partial, 110 | tabSpace = '', 111 | isNoOther = false, 112 | progressHandle: null | (() => void) = null 113 | ) { 114 | let html = ''; 115 | const space = tabSpace + ' '; 116 | for (let i = 0; i < arr.length; i++) { 117 | const item = arr[i]; 118 | if (item.children) { 119 | if (settings.noParentFolders && !['1', '2', 'menu________', 'toolbar_____', 'unfiled_____', 'mobile______'].includes(item.id)) { 120 | html += `${await traverse(item.children, settings, '', isNoOther, progressHandle)}`; 121 | } else if (settings.noOtherBookmarks && ['2', 'toolbar_____', 'unfiled_____', 'mobile______'].includes(item.id)) { 122 | html += `${await traverse(item.children, settings, '', true, progressHandle)}`; 123 | } else { 124 | html += ` 125 | ${space}
${htmlEncode(item.title)} 135 | ${space}

${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}


`; 144 | } else { 145 | html += ` 146 | ${settings.noParentFolders ? (isNoOther ? ' ' : ' ') : space}
${htmlEncode(item.title)}`; 149 | } 150 | 151 | if (typeof progressHandle === 'function') { 152 | progressHandle(); 153 | } 154 | } 155 | } 156 | 157 | return html; 158 | } 159 | 160 | export async function htmlFileGenerator( 161 | arr: chrome.bookmarks.BookmarkTreeNode[], 162 | settings: Partial, 163 | progressHandle: null | (() => void) = null, 164 | progressCompleted: null | (() => void) = null 165 | ) { 166 | const header = ` 167 | 170 | 171 | Bookmarks 172 |

Bookmarks

173 | `; 174 | const body = `

${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 | 101 | 102 | 132 | 133 | 150 | -------------------------------------------------------------------------------- /src/img/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightAPIs/free-export-bookmarks/4432500967313b7bf5ee278eb4047ac6e991460d/src/img/icon-24.png -------------------------------------------------------------------------------- /src/index/App.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 237 | 238 | 270 | -------------------------------------------------------------------------------- /src/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Index 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from '@/common/i18n'; 2 | import { createApp } from 'vue'; 3 | import { createPinia } from 'pinia'; 4 | import App from './App.vue'; 5 | import 'element-plus/theme-chalk/dark/css-vars.css'; 6 | 7 | document.title = i18n('indexTitle'); 8 | const isChromium = import.meta.env.IS_CHROMIUM; 9 | 10 | const pinia = createPinia(); 11 | pinia.use(({ store }) => { 12 | store.$subscribe((mutation, state) => { 13 | if (mutation.storeId === 'settings' && mutation.type === 'direct') { 14 | chrome.storage.local.set({ 15 | settings: isChromium ? state : Object.assign({}, state), 16 | }); 17 | } 18 | }); 19 | }); 20 | 21 | const app = createApp(App); 22 | app.use(pinia); 23 | app.mount('#app'); 24 | -------------------------------------------------------------------------------- /src/manifest/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.6.0", 6 | "minimum_chrome_version": "104", 7 | "author": { 8 | "email": "no.web.developer@gmail.com" 9 | }, 10 | "homepage_url": "https://github.com/LightAPIs/free-export-bookmarks", 11 | "default_locale": "en", 12 | "icons": { 13 | "16": "icons/app/icon-16.png", 14 | "19": "icons/app/icon-19.png", 15 | "24": "icons/app/icon-24.png", 16 | "32": "icons/app/icon-32.png", 17 | "48": "icons/app/icon-48.png", 18 | "64": "icons/app/icon-64.png", 19 | "96": "icons/app/icon-96.png", 20 | "128": "icons/app/icon-128.png" 21 | }, 22 | "background": { 23 | "service_worker": "background/index.js", 24 | "type": "module" 25 | }, 26 | "action": { 27 | "default_title": "__MSG_extensionName__\n__MSG_extensionTip__" 28 | }, 29 | "permissions": [ 30 | "bookmarks", 31 | "storage", 32 | "favicon", 33 | "downloads" 34 | ], 35 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0upo8Ezr2Eqq/E82nd93iHGwt/Dqh6Pr51EncxgJiWXeESiKltG/SChAQL9l2+ymp9eDAJgIXYsq23vGk6jPWa1+hKNdDX212OH02FSAJhNvlUFJu/2zVdXchi3XnPRq7IsEZnR1gO9BPDa58Vl18KBKT+MPN+KDRNTUXVDqx4MyB0zaDXOsqFrhYYAGUxc2LkhgSedyT6UYh3efdEe/Ebtaia1GO/bgVHj5pcUotHSiZvfLEnXPoyoXTbzaoe7hTAjAuZ+ixng6oVdNRRBfOgQb2IiqDnxkLzJKHQL190UUgLY0mzAdWmu0GanyyAST/8DYuVZWPQzJ8vDSiMTnJwIDAQAB" 36 | } 37 | -------------------------------------------------------------------------------- /src/manifest/edge/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.6.0", 6 | "minimum_chrome_version": "104", 7 | "author": { 8 | "email": "no.web.developer@gmail.com" 9 | }, 10 | "homepage_url": "https://github.com/LightAPIs/free-export-bookmarks", 11 | "default_locale": "en", 12 | "icons": { 13 | "16": "icons/app/icon-16.png", 14 | "19": "icons/app/icon-19.png", 15 | "24": "icons/app/icon-24.png", 16 | "32": "icons/app/icon-32.png", 17 | "48": "icons/app/icon-48.png", 18 | "64": "icons/app/icon-64.png", 19 | "96": "icons/app/icon-96.png", 20 | "128": "icons/app/icon-128.png" 21 | }, 22 | "background": { 23 | "service_worker": "background/index.js", 24 | "type": "module" 25 | }, 26 | "action": { 27 | "default_title": "__MSG_extensionName__\n__MSG_extensionTip__" 28 | }, 29 | "permissions": [ 30 | "bookmarks", 31 | "storage", 32 | "favicon", 33 | "downloads" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/manifest/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.6.0", 6 | "author": "dragonish", 7 | "homepage_url": "https://github.com/LightAPIs/free-export-bookmarks", 8 | "default_locale": "en", 9 | "icons": { 10 | "16": "icons/app/icon-16.png", 11 | "19": "icons/app/icon-19.png", 12 | "24": "icons/app/icon-24.png", 13 | "32": "icons/app/icon-32.png", 14 | "48": "icons/app/icon-48.png", 15 | "64": "icons/app/icon-64.png", 16 | "96": "icons/app/icon-96.png", 17 | "128": "icons/app/icon-128.png" 18 | }, 19 | "background": { 20 | "scripts": [ 21 | "background/index.js" 22 | ], 23 | "type": "module" 24 | }, 25 | "browser_specific_settings": { 26 | "gecko": { 27 | "id": "{d63ee338-4bfe-49ce-b7f4-d368b4db3128}" 28 | } 29 | }, 30 | "action": { 31 | "default_title": "__MSG_extensionName__ - __MSG_extensionTip__" 32 | }, 33 | "permissions": [ 34 | "bookmarks", 35 | "storage", 36 | "menus", 37 | "downloads" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useSettingsStore = defineStore('settings', { 4 | state: (): Settings => ({ 5 | showIcon: false, 6 | autoExpandAll: false, 7 | includeIcon: false, 8 | includeDate: false, 9 | noOtherBookmarks: false, 10 | noParentFolders: false, 11 | saveAs: false, 12 | }), 13 | 14 | actions: { 15 | async read() { 16 | const res = await chrome.storage.local.get('settings'); 17 | this.$patch(res.settings || {}); 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly BROWSER: string; 5 | readonly IS_CHROMIUM: boolean; 6 | readonly VERSION: string; 7 | } 8 | 9 | interface Settings { 10 | showIcon: boolean; 11 | autoExpandAll: boolean; 12 | includeIcon: boolean; 13 | includeDate: boolean; 14 | noOtherBookmarks: boolean; 15 | noParentFolders: boolean; 16 | saveAs: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /test/common/expected/firefoxSeparator.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks Toolbar

10 |

11 |

About 12 |

13 |

Bookmarks Menu

14 |

15 |

Google 16 |
17 |
GitHub 18 |

19 |

20 | -------------------------------------------------------------------------------- /test/common/expected/folder.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

Folder

13 |

14 |

GitHub 15 |

16 |

17 |

18 | -------------------------------------------------------------------------------- /test/common/expected/folderIncludeDate.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

Folder

13 |

14 |

GitHub 15 |

16 |

17 |

18 | -------------------------------------------------------------------------------- /test/common/expected/includeDate.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

13 |

14 | -------------------------------------------------------------------------------- /test/common/expected/includeIcon.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

13 |

14 | -------------------------------------------------------------------------------- /test/common/expected/noOtherBookmarks.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

Folder

13 |

14 |

GitHub 15 |

16 |

17 |

Home / X 18 |

19 | -------------------------------------------------------------------------------- /test/common/expected/noParentFolders.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |
GitHub 13 |

14 |

15 | -------------------------------------------------------------------------------- /test/common/expected/other.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

Folder

13 |

14 |

GitHub 15 |

16 |

17 |

Other bookmarks

18 |

19 |

Home / X 20 |

21 |

22 | -------------------------------------------------------------------------------- /test/common/expected/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Bookmarks 7 |

Bookmarks

8 |

9 |

Bookmarks bar

10 |

11 |

Google 12 |

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 | --------------------------------------------------------------------------------