├── .c8rc ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .htmlvalidate.json ├── .stylelintrc ├── LICENSE ├── README.fa.md ├── README.ja.md ├── README.md ├── eslint.config.mjs ├── index.js ├── package-lock.json ├── package.json ├── resource ├── alpenglow-manifest.json ├── dark-manifest.json └── light-manifest.json ├── scripts ├── commander.js ├── common.js └── file-util.js ├── src ├── LICENSE ├── _locales │ ├── en │ │ └── messages.json │ ├── en_CA │ │ └── messages.json │ ├── en_GB │ │ └── messages.json │ ├── fa │ │ └── messages.json │ └── ja │ │ └── messages.json ├── css │ ├── options.css │ └── sidebar.css ├── html │ ├── options.html │ └── sidebar.html ├── img │ ├── addons-favicon.svg │ ├── audio-muted.svg │ ├── audio-play.svg │ ├── briefcase.svg │ ├── cart.svg │ ├── chill.svg │ ├── circle.svg │ ├── close.svg │ ├── customize-favicon.svg │ ├── default-favicon.svg │ ├── dollar.svg │ ├── drop-arrow.svg │ ├── edit.svg │ ├── fence.svg │ ├── fingerprint.svg │ ├── folder.svg │ ├── food.svg │ ├── fruit.svg │ ├── gift.svg │ ├── hourglass.svg │ ├── icon.svg │ ├── loading.svg │ ├── new-tab.svg │ ├── options-favicon.svg │ ├── pet.svg │ ├── pin.svg │ ├── spacer.svg │ ├── tree.svg │ ├── twitter-logo-blue.svg │ └── vacation.svg ├── lib │ ├── color │ │ ├── LICENSE │ │ ├── css-color.min.js │ │ ├── css-color.min.js.map │ │ └── package.json │ ├── purify │ │ ├── LICENSE │ │ ├── package.json │ │ ├── purify.min.js │ │ └── purify.min.js.map │ ├── tldts │ │ ├── LICENSE │ │ ├── index.esm.min.js │ │ ├── index.esm.min.js.map │ │ └── package.json │ └── url │ │ ├── LICENSE │ │ ├── package.json │ │ ├── url-sanitizer-wo-dompurify.min.js │ │ └── url-sanitizer-wo-dompurify.min.js.map ├── manifest.json ├── mjs │ ├── background-main.js │ ├── background.js │ ├── bookmark.js │ ├── browser-tabs.js │ ├── browser.js │ ├── color.js │ ├── common.js │ ├── constant.js │ ├── localize.js │ ├── main.js │ ├── menu-items.js │ ├── menu.js │ ├── options-main.js │ ├── options.js │ ├── package.json │ ├── port.js │ ├── session.js │ ├── sidebar.js │ ├── tab-content.js │ ├── tab-dnd.js │ ├── tab-group.js │ ├── theme.js │ └── util.js └── web-ext-config.cjs ├── test ├── background-main.test.js ├── bookmark.test.js ├── browser-tabs.test.js ├── color.test.js ├── commander.test.js ├── common-mjs.test.js ├── common.test.js ├── constant.test.js ├── file-util.test.js ├── file │ └── test.txt ├── localize.test.js ├── main.test.js ├── menu-items.test.js ├── menu.test.js ├── mocha │ └── setup.js ├── options-main.test.js ├── port.test.js ├── session.test.js ├── tab-content.test.js ├── tab-dnd.test.js ├── tab-group.test.js ├── theme.test.js └── util.test.js ├── tsconfig.json └── types ├── index.d.ts ├── scripts ├── commander.d.ts ├── common.d.ts └── file-util.d.ts └── src ├── lib ├── color │ └── css-color.min.d.ts ├── tldts │ └── index.esm.min.d.ts └── url │ └── url-sanitizer-wo-dompurify.min.d.ts └── mjs ├── background-main.d.ts ├── background.d.ts ├── bookmark.d.ts ├── browser-tabs.d.ts ├── browser.d.ts ├── color.d.ts ├── common.d.ts ├── constant.d.ts ├── localize.d.ts ├── main.d.ts ├── menu-items.d.ts ├── menu.d.ts ├── options-main.d.ts ├── options.d.ts ├── port.d.ts ├── session.d.ts ├── sidebar.d.ts ├── tab-content.d.ts ├── tab-dnd.d.ts ├── tab-group.d.ts ├── theme.d.ts └── util.d.ts /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["src/mjs/browser.js", "src/lib/*", "test/*"], 3 | "reporter": "text" 4 | } 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://paypal.me/asamuzakjp'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | permissions: 6 | contents: read 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | node-version: [ lts/*, latest ] 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | check-latest: true 27 | - run: npm ci 28 | - run: npm run lint 29 | - run: npm run testall 30 | -------------------------------------------------------------------------------- /.htmlvalidate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "html-validate:standard" 4 | ], 5 | "elements": [ 6 | "html5", 7 | { 8 | "details": { 9 | "requiredContent": [] 10 | }, 11 | "th": { 12 | "attributes": { 13 | "scope": { 14 | "required": false 15 | } 16 | } 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "no-descending-specificity": null, 5 | "selector-id-pattern": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /README.fa.md: -------------------------------------------------------------------------------- 1 | [EN](./README.md) | [JA](./README.ja.md) | فا 2 | 3 | [![build](https://github.com/asamuzaK/sidebarTabs/workflows/build/badge.svg)](https://github.com/asamuzaK/sidebarTabs/actions?query=workflow%3Abuild) 4 | [![CodeQL](https://github.com/asamuzaK/sidebarTabs/workflows/CodeQL/badge.svg)](https://github.com/asamuzaK/sidebarTabs/actions?query=workflow%3ACodeQL) 5 | [![Mozilla Add-on](https://img.shields.io/amo/v/sidebarTabs@asamuzak.jp.svg)](https://addons.mozilla.org/firefox/addon/sidebartabs/) 6 | 7 | # زبانه‌های افقی 8 | 9 | افزونه‌ای برای فایرفاکس 10 | که زبانه‌ها را به صورت افقی در کنار صفحه قرار می‌دهد... 11 | * نمایش زبانه‌ها به صورت افقی. 12 | * گروه‌بندی کردن زبانه‌ها و جمع/باز کردنشان. 13 | 14 | ## دریافت 15 | 16 | * [زبانه‌های افقی – افزونه‌ای برای فایرفاکس](https://addons.mozilla.org/firefox/addon/sidebartabs/ "Sidebar Tabs – Add-ons for Firefox") 17 | 18 | ## about:config 19 | 20 | برخی از ویژگی‌های آزمایشی فایرفاکس در این افزونه به کار رفته‌اند. بنابراین برای استفاده کامل از این افزونه (مثلاً برای قابلیت پوسته تاریک)،‌ باید این کار را انجام بدهید: 21 | * مقدار `svg.context-properties.content.enabled` را روی `true` تنظیم کنید. 22 | 23 | این کار میتواند باعث بهبود پوسته تاریک هم بشود ([مشکل #۱۵۴](https://github.com/asamuzaK/sidebarTabs/issues/154)). 24 | 25 | ## گروه‌بندی زبانه‌ها 26 | 27 | * زبانه‌های موردنظر را با "شیفت + کلیک چپ" یا "کنترل + کلیک چپ" ("کامند + کلیک چپ" در مک) روی هر زبانه انتخاب کنید. 28 | * یکی از زبانه‌های انتخاب شده را با "شیفت + کلیک چپ" بکشید و روی زبانه ای که میخواهید گروه‌بندی کنید، بندازید. 29 | * همچنین شما می‌توانید با انتخاب گزینه "گروه‌ زبانه" از فهرست راست کلیک و سپس انتخاب گزینه "گروه‌بندی زبانه‌های انتخاب شده" عمل گروه‌بندی را انجام بدهید. 30 | * گروه زبانه‌ها هر کدام رنگ‌بندی خاص خودشان را دارند. 31 | * گروه زبانه‌ها را می‌توان با کلیک کردن روی بخش رنگی و یا از فهرست راست کلیک جمع/باز کرد. 32 | * زبانه جدیدی که از یک زبانه داخل گروه باز بشود،‌ عضوی از همان گروه زبانه خواهد شد. 33 | * برای لغو گروه زبانه، میتوانید از فهرست راست کلیک اقدام کنید. 34 | * برای جابجا کردن گروهی از زبانه ها با کشیدن‌و رها کردن (Drag and drop)، کلید های Shift + Ctrl (یا Shift + Cmd در مک) را نگه دارید، گروه زبانه ها را بکشید و رها کنید. 35 | * گروه زبانه‌ها در صفحات خصوصی (Private window) ذخیره نخواهند شد. 36 | 37 | ## مشکلات موجود 38 | * امکان پنهان کردن زبانه‌های خود مرورگر وجود ندارد زیرا API (واسط برنامه‌نویسی) برای این کار در افزونه‌ها تعبیه نشده است اما شما می‌توانید به صورت دستی این کار را انجام بدهید. 39 | [مشکل #5](https://github.com/asamuzaK/sidebarTabs/issues/5 "افزودن قابلیت \"پنهان کردن زبانه‌های خود مرورگر\" · مشکل #5 · asamuzaK/sidebarTabs") 40 | * گزینه "فرستادن زبانه به دستگاه دیگر" از فهرست راست کلیک اصلی در این افزونه تعبیه نشده است زیرا API (واسط برنامه‌نویسی) برای آن در افزونه‌ها وجود ندارد. 41 | [مشکل #7](https://github.com/asamuzaK/sidebarTabs/issues/7 "افزودن قابلیت \"فرستادن زبانه به دستگاه دیگر\" · Issue #7 · asamuzaK/sidebarTabs") 42 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | [EN](./README.md) | JA | [فا](./README.fa.md) 2 | 3 | [![build](https://github.com/asamuzaK/sidebarTabs/workflows/build/badge.svg)](https://github.com/asamuzaK/sidebarTabs/actions?query=workflow%3Abuild) 4 | [![CodeQL](https://github.com/asamuzaK/sidebarTabs/workflows/CodeQL/badge.svg)](https://github.com/asamuzaK/sidebarTabs/actions?query=workflow%3ACodeQL) 5 | [![Mozilla Add-on](https://img.shields.io/amo/v/sidebarTabs@asamuzak.jp.svg)](https://addons.mozilla.org/firefox/addon/sidebartabs/) 6 | 7 | # sidebarTabs 8 | 9 | Firefox用の拡張機能。 10 | サイドバーにタブをエミュレートした上で、 11 | * タブを縦に並べて表示します。 12 | * タブをグループ化して、折りたたみ・展開することが可能です。 13 | 14 | ## ダウンロード 15 | 16 | * [サイドバータブ – Firefox 向けアドオン](https://addons.mozilla.org/firefox/addon/sidebartabs/ "サイドバータブ – Firefox 向けアドオン") 17 | 18 | ## about:config 19 | 20 | 試験的な機能を使っているので以下の設定を有効化する必要があります。 21 | 22 | * `svg.context-properties.content.enabled`を`true`にセットする。 23 | 24 | これにより、暗いテーマも改善できます([Issue #154](https://github.com/asamuzaK/sidebarTabs/issues/154))。 25 | 26 | ## タブグループ 27 | 28 | * 「Shift + 左クリック」または「Ctrl + 左クリック」(Macでは「Cmd + 左クリック」)でグループ化したいタブを選択状態にします。 29 | * 選択されたタブのどれかを「Shift + 左クリック」でドラッグして、親としてグループ化したいタブの上にドロップします。 30 | * コンテキストメニューから「タブグループ」を開き「選択したタブのグループ化」を選んでもグループ化することができます。 31 | * タブグループは、グループごとに色分け表示されます。 32 | * タブグループの折りたたみ・展開は色のついた部分をクリックするか、コンテキストメニューから実行できます。 33 | * タブグループの中にあるタブから開いた新規タブはそのグループのタブとして表示されます。 34 | * タブのグループ化を解除するには、コンテキストメニューから実行してください。 35 | * ドラッグ & ドロップ(DnD)でタブグループを移動するには「Shift + Ctrl + DnD」(Macでは「Shift + Cmd + DnD」)。 36 | * プライベートブラウジング中のタブグループに関するデータは保存されません。 37 | 38 | ## 既知の問題 39 | 40 | * WebExtensionsにAPIがないため、ブラウザの本来のタブバーを非表示にすることはできません。ただし、手動で非表示にすることはできます。 41 | [Issue #5](https://github.com/asamuzaK/sidebarTabs/issues/5 "Add ability to \"hide native tab bars\" · Issue #5 · asamuzaK/sidebarTabs") 42 | * 本来のタブのコンテキストメニューにある「タブを端末に送る」の項目は、WebExtensionsにAPIがないため実装していません。 43 | [Issue #7](https://github.com/asamuzaK/sidebarTabs/issues/7 "Add \"Send tab to device\" functionalty · Issue #7 · asamuzaK/sidebarTabs") 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | EN | [JA](./README.ja.md) | [فا](./README.fa.md) 2 | 3 | [![build](https://github.com/asamuzaK/sidebarTabs/workflows/build/badge.svg)](https://github.com/asamuzaK/sidebarTabs/actions?query=workflow%3Abuild) 4 | [![CodeQL](https://github.com/asamuzaK/sidebarTabs/workflows/CodeQL/badge.svg)](https://github.com/asamuzaK/sidebarTabs/actions?query=workflow%3ACodeQL) 5 | [![Mozilla Add-on](https://img.shields.io/amo/v/sidebarTabs@asamuzak.jp.svg)](https://addons.mozilla.org/firefox/addon/sidebartabs/) 6 | 7 | # sidebarTabs 8 | 9 | WebExtensions for Firefox. 10 | 11 | * Display tabs vertically in the sidebar. 12 | * Tabs can be grouped. 13 | 14 | ## Download 15 | 16 | * [Sidebar Tabs – Add-ons for Firefox](https://addons.mozilla.org/firefox/addon/sidebartabs/ "Sidebar Tabs – Add-ons for Firefox") 17 | 18 | ## Browser settings 19 | 20 | To match icon colors to your theme, enable the experimental feature as follows: 21 | 22 | * Visit `about:config`. 23 | * Search for `svg.context-properties.content.enabled` and set the value to `true`. 24 | 25 | This can also improve dark theme ([Issue #154](https://github.com/asamuzaK/sidebarTabs/issues/154)). 26 | 27 | ## Tab groups 28 | 29 | * To group tabs: 30 | * Select tabs by `Shift` + click or `Ctrl`[^1] + click. 31 | * Drag one of the selected tabs and `Shift` + drop it on the tab you want to group. 32 | * Or from the context menu (right click on one of the selected tabs), select "Tab Group" -> "Group Selected Tabs". 33 | * To add group label: 34 | * From the context menu, select "Tab Group" -> "Show Group Label" and edit. 35 | * To collapse / expand tab group: 36 | * Click on the colored part will toggle collapsed / expanded state. 37 | * Or from the context menu, select "Tab Group" -> "Collapse (Expand) Tab Group". 38 | * To cancel tab group: 39 | * From the context menu, select "Tab Group" -> "Ungroup tabs". 40 | 41 | Tab groups will not be saved during private browsing. 42 | 43 | ## Drag and drop 44 | 45 | There are some differences in Firefox's native tabs and Sidebar Tabs. 46 | 47 | |Command|Drag item|Drop target|Note| 48 | |----|----|----|----| 49 | |Move Tab|Drag[^3] Tab|Drop[^4] to Sidebar|Also between windows[^5]| 50 | |Copy Tab|Drag[^3] Tab|`Ctrl`[^1] + Drop[^4] to Sidebar|Also between windows[^5]| 51 | |Move Tab Group|`Shift` + `Ctrl`[^1] + Drag Tab Group|Drop[^4] to Sidebar|Also between windows[^5]| 52 | |Copy Tab Group|`Shift` + `Ctrl`[^1] + Drag Tab Group|`Ctrl`[^1] + Drop[^4] to Sidebar|Also between windows[^5]| 53 | |Open URL|Drag URL|Drop to Sidebar| | 54 | |Search Text|Drag Text|Drop to Sidebar | 55 | |Create Bookmark|`Alt`[^2] + Drag[^3] Tab|Drop to Bookmark Toolbar|Without `Alt`[^2], D&D to Bookmark Toolbar has no effect| 56 | |Open in New Window|N/A|N/A|Not available. Instead, use `Move Tab` -> `Move to New Window` context menu to open the tab in a new window. **Caveat**: `Alt`[^2] + drag[^3] tab and drop to desktop creates internet shortcut on desktop| 57 | 58 | ## Known Issues 59 | 60 | * The extension can't hide the browser native tab bars because there is no API for that in WebExtensions. But you can hide them manually. 61 | [Issue #5](https://github.com/asamuzaK/sidebarTabs/issues/5 "Add ability to \"hide native tab bars\" · Issue #5 · asamuzaK/sidebarTabs") 62 | * The context menu item of the original tab "Send tab to device" is not implemented because there is no API for that in WebExtensions. 63 | [Issue #7](https://github.com/asamuzaK/sidebarTabs/issues/7 "Add \"Send tab to device\" functionalty · Issue #7 · asamuzaK/sidebarTabs") 64 | 65 | [^1]: `Cmd` on Mac 66 | [^2]: `Opt` on Mac 67 | [^3]: `Shift` + Drag / `Ctrl` (`Cmd` on Mac) + Drag selects multiple tabs. 68 | [^4]: `Shift` + Drop will group dragged tab(s) and drop target. 69 | [^5]: Grouping will be canceled, `Shift` has no effect. 70 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import jsdoc from 'eslint-plugin-jsdoc'; 2 | import nounsanitized from 'eslint-plugin-no-unsanitized'; 3 | import regexp from 'eslint-plugin-regexp'; 4 | import unicorn from 'eslint-plugin-unicorn'; 5 | import globals from 'globals'; 6 | import neostandard, { plugins as neostdplugins } from 'neostandard'; 7 | 8 | export default [ 9 | ...neostandard({ 10 | semi: true 11 | }), 12 | jsdoc.configs['flat/recommended'], 13 | nounsanitized.configs.recommended, 14 | regexp.configs['flat/recommended'], 15 | { 16 | ignores: ['src/lib', 'src/web-ext-config.cjs', '**/*.min.js'] 17 | }, 18 | { 19 | languageOptions: { 20 | globals: { 21 | ...globals.browser, 22 | ...globals.node, 23 | ...globals.webextensions 24 | } 25 | }, 26 | linterOptions: { 27 | reportUnusedDisableDirectives: true 28 | }, 29 | plugins: { 30 | '@stylistic': neostdplugins['@stylistic'], 31 | nounsanitized, 32 | regexp, 33 | unicorn 34 | }, 35 | rules: { 36 | '@stylistic/space-before-function-paren': ['error', { 37 | anonymous: 'always', 38 | asyncArrow: 'always', 39 | named: 'never' 40 | }], 41 | 'import-x/order': ['error', { 42 | alphabetize: { 43 | order: 'ignore', 44 | caseInsensitive: false 45 | } 46 | }], 47 | 'no-await-in-loop': 'error', 48 | 'no-use-before-define': ['error', { 49 | allowNamedExports: false, 50 | classes: true, 51 | functions: true, 52 | variables: true 53 | }], 54 | 'unicorn/prefer-node-protocol': 'error' 55 | } 56 | } 57 | ]; 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | */ 4 | 5 | /* api */ 6 | import process from 'node:process'; 7 | import { parseCommand } from './scripts/commander.js'; 8 | import { logErr, throwErr } from './scripts/common.js'; 9 | 10 | /* process */ 11 | process.on('uncaughtException', throwErr); 12 | process.on('unhandledRejection', logErr); 13 | 14 | parseCommand(process.argv); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sidebartabs", 3 | "description": "Emulate tabs in sidebar.", 4 | "author": "asamuzaK", 5 | "license": "MPL-2.0", 6 | "homepage": "https://github.com/asamuzaK/sidebarTabs", 7 | "bugs": { 8 | "url": "https://github.com/asamuzaK/sidebarTabs/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/asamuzaK/sidebarTabs.git" 13 | }, 14 | "type": "module", 15 | "dependencies": { 16 | "@asamuzakjp/css-color": "^3.2.0", 17 | "dompurify": "^3.2.6", 18 | "tldts-experimental": "^7.0.7", 19 | "url-sanitizer": "^2.0.9", 20 | "webext-schema": "^5.6.0" 21 | }, 22 | "devDependencies": { 23 | "@asamuzakjp/dom-selector": "^6.5.0", 24 | "@types/firefox-webext-browser": "^120.0.4", 25 | "@types/node": "^22.15.21", 26 | "addons-linter": "^7.13.0", 27 | "c8": "^10.1.3", 28 | "camelize": "^1.0.1", 29 | "commander": "^14.0.0", 30 | "copyfiles": "^2.4.1", 31 | "decamelize": "^6.0.0", 32 | "eslint": "^9.27.0", 33 | "eslint-plugin-jsdoc": "^50.6.17", 34 | "eslint-plugin-no-unsanitized": "^4.1.2", 35 | "eslint-plugin-regexp": "^2.7.0", 36 | "eslint-plugin-unicorn": "^59.0.1", 37 | "globals": "^16.2.0", 38 | "html-validate": "^9.5.5", 39 | "jsdom": "^26.1.0", 40 | "mocha": "^11.5.0", 41 | "neostandard": "^0.12.1", 42 | "npm-run-all2": "^8.0.4", 43 | "sinon": "^20.0.0", 44 | "stylelint": "^16.19.1", 45 | "stylelint-config-standard": "^38.0.0", 46 | "typescript": "^5.8.3", 47 | "undici": "^7.10.0" 48 | }, 49 | "scripts": { 50 | "include": "npm-run-all -s include:*", 51 | "include:browser": "copyfiles --up=3 --verbose node_modules/webext-schema/modules/browser.js src/mjs", 52 | "include:color": "copyfiles -f --verbose node_modules/@asamuzakjp/css-color/LICENSE node_modules/@asamuzakjp/css-color/dist/browser/css-color.min.js node_modules/@asamuzakjp/css-color/dist/browser/css-color.min.js.map src/lib/color && node index include --dir=color -i", 53 | "include:purify": "copyfiles -f --verbose node_modules/dompurify/LICENSE node_modules/dompurify/dist/purify.min.js node_modules/dompurify/dist/purify.min.js.map src/lib/purify && node index include --dir=purify -i", 54 | "include:tldts": "copyfiles -f --verbose node_modules/tldts-experimental/LICENSE node_modules/tldts-experimental/dist/index.esm.min.js node_modules/tldts-experimental/dist/index.esm.min.js.map src/lib/tldts && node index include --dir=tldts -i", 55 | "include:url": "copyfiles -f --verbose node_modules/url-sanitizer/LICENSE node_modules/url-sanitizer/dist/url-sanitizer-wo-dompurify.min.js node_modules/url-sanitizer/dist/url-sanitizer-wo-dompurify.min.js.map src/lib/url && node index include --dir=url -i", 56 | "lint": "npm-run-all -s lint:*", 57 | "lint:addons-linter": "addons-linter src", 58 | "lint:eslint": "eslint . --fix", 59 | "lint:html": "html-validate src/html/*.html", 60 | "lint:style": "stylelint src/css/*.css --fix", 61 | "test": "npm run test:central", 62 | "test:beta": "c8 mocha --require=test/mocha/setup.js --channel=beta --exit test/*.test.js", 63 | "test:central": "c8 mocha --require=test/mocha/setup.js --channel=central --exit test/*.test.js", 64 | "test:esr": "c8 mocha --require=test/mocha/setup.js --channel=esr --exit test/*.test.js", 65 | "test:release": "c8 mocha --require=test/mocha/setup.js --channel=release --exit test/*.test.js", 66 | "testall": "npm-run-all -s test:*", 67 | "tsc": "node index clean --dir=types -i && npx tsc", 68 | "update": "node index update -i", 69 | "update-alpen": "node index update --dir=alpenglow -i", 70 | "update-dark": "node index update --dir=dark -i", 71 | "update-light": "node index update --dir=light -i" 72 | }, 73 | "version": "16.0.3" 74 | } 75 | -------------------------------------------------------------------------------- /resource/alpenglow-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "browser_specific_settings": { 5 | "gecko": { 6 | "id": "firefox-alpenglow@mozilla.org" 7 | } 8 | }, 9 | 10 | "name": "Firefox Alpenglow", 11 | "description": "Use a colorful appearance for buttons, menus, and windows.", 12 | "version": "1.5", 13 | "icons": { "32": "icon.svg" }, 14 | 15 | "theme": { 16 | "images": { 17 | "additional_backgrounds": [ 18 | "background-noodles-right.svg", 19 | "background-noodles-left.svg", 20 | "background-gradient.svg" 21 | ] 22 | }, 23 | 24 | "properties": { 25 | "additional_backgrounds_alignment": [ 26 | "right top", 27 | "left top", 28 | "right top" 29 | ], 30 | "additional_backgrounds_tiling": ["no-repeat", "no-repeat", "repeat-x"], 31 | "zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)" 32 | }, 33 | "colors": { 34 | "frame": "hsla(240, 20%, 98%, 1)", 35 | "toolbar": "hsla(0, 0%, 100%, .76)", 36 | "button_background_active": "hsla(240, 26%, 11%, .16)", 37 | "button_background_hover": "hsla(240, 26%, 11%, .08)", 38 | "toolbar_text": "hsla(258, 66%, 48%, 1)", 39 | "icons_attention": "hsla(180, 100%, 32%, 1)", 40 | "toolbar_vertical_separator": "hsla(261, 53%, 15%, .2)", 41 | "toolbar_field": "hsla(0, 0%, 100%, .8)", 42 | "toolbar_field_focus": "hsla(261, 53%, 15%, .96)", 43 | "toolbar_field_text": "hsla(261, 53%, 15%, 1)", 44 | "toolbar_field_text_focus": "hsla(255, 100%, 94%, 1)", 45 | "toolbar_field_border": "transparent", 46 | "toolbar_field_border_focus": "hsla(265, 100%, 72%, 1)", 47 | "toolbar_field_highlight": "hsla(265, 100%, 72%, .32)", 48 | "toolbar_top_separator": "transparent", 49 | "toolbar_bottom_separator": "hsla(261, 53%, 15%, .32)", 50 | "bookmark_text": "hsla(261, 53%, 15%, 1)", 51 | "tab_text": "hsla(261, 53%, 15%, 1)", 52 | "tab_background_text": "hsla(261, 53%, 15%, 1)", 53 | "tab_background_separator": "hsla(261, 53%, 15%, 1)", 54 | "tab_line": "hsla(265, 100%, 72%, 1)", 55 | "tab_loading": "hsla(265, 100%, 72%, 1)", 56 | "ntp_background": "#F9F9FB", 57 | "ntp_text": "hsla(261, 53%, 15%, 1)", 58 | "popup": "hsla(254, 46%, 21%, 1)", 59 | "popup_text": "hsla(255, 100%, 94%, 1)", 60 | "popup_border": "hsla(255, 100%, 94%, .32)", 61 | "popup_highlight": "hsla(255, 100%, 94%, .12)", 62 | "popup_highlight_text": "hsla(0, 0%, 100%, 1)", 63 | "sidebar": "rgb(255, 255, 255)", 64 | "sidebar_text": "hsla(261, 53%, 15%, 1)", 65 | "sidebar_border": "hsla(261, 53%, 15%, .24)", 66 | "sidebar_highlight": "hsla(265, 100%, 72%, 1)", 67 | "sidebar_highlight_text": "hsla(0, 0%, 100%, 1)", 68 | "focus_outline": "hsla(258, 65%, 48%, 1)" 69 | } 70 | }, 71 | "dark_theme": { 72 | "images": { 73 | "additional_backgrounds": [ 74 | "background-noodles-right-dark.svg", 75 | "background-noodles-left-dark.svg", 76 | "background-gradient-dark.svg" 77 | ] 78 | }, 79 | 80 | "properties": { 81 | "additional_backgrounds_alignment": [ 82 | "right top", 83 | "left top", 84 | "right top" 85 | ], 86 | "additional_backgrounds_tiling": ["no-repeat", "no-repeat", "repeat-x"], 87 | "zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)" 88 | }, 89 | "colors": { 90 | "frame": "#291D4F", 91 | "toolbar": "hsla(254, 46%, 21%, .96)", 92 | "button_background_active": "hsla(255, 100%, 94%, .24)", 93 | "button_background_hover": "hsla(255, 100%, 94%, .12)", 94 | "icons": "hsla(271, 100%, 77%, 1)", 95 | "icons_attention": "hsla(157, 100%, 66%, 1)", 96 | "toolbar_text": "hsla(255, 100%, 94%, 1)", 97 | "toolbar_vertical_separator": "hsla(271, 100%, 77%, .4)", 98 | "toolbar_field": "hsla(250, 43%, 25%, 1)", 99 | "toolbar_field_focus": "hsla(250, 43%, 25%, .98)", 100 | "toolbar_field_text": "hsla(255, 100%, 94%, 1)", 101 | "toolbar_field_text_focus": "hsla(255, 100%, 94%, 1)", 102 | "toolbar_field_border": "transparent", 103 | "toolbar_field_border_focus": "hsla(265, 100%, 72%, 1)", 104 | "toolbar_field_highlight": "hsla(265, 100%, 72%, .32)", 105 | "toolbar_top_separator": "transparent", 106 | "toolbar_bottom_separator": "hsla(245, 38%, 33%, .96)", 107 | "bookmark_text": "hsla(255, 100%, 94%, 1)", 108 | "tab_selected": "rgb(60, 31, 123)", 109 | "tab_text": "hsla(255, 100%, 94%, 1)", 110 | "tab_background_text": "hsla(255, 100%, 94%, 1)", 111 | "tab_background_separator": "hsla(255, 100%, 94%, 1)", 112 | "tab_line": "hsla(265, 100%, 72%, 1)", 113 | "tab_loading": "hsla(265, 100%, 72%, 1)", 114 | "ntp_background": "#2A2A2E", 115 | "ntp_text": "hsla(255, 100%, 94%, 1)", 116 | "popup": "hsla(250, 43%, 25%, 1)", 117 | "popup_text": "hsla(255, 100%, 94%, 1)", 118 | "popup_border": "hsla(255, 100%, 94%, .32)", 119 | "popup_highlight": "hsla(255, 100%, 94%, .12)", 120 | "popup_highlight_text": "hsla(0, 0%, 100%, 1)", 121 | "sidebar": "hsla(250, 43%, 25%, 1)", 122 | "sidebar_text": "hsla(255, 100%, 94%, 1)", 123 | "sidebar_border": "hsla(255, 100%, 94%, .24)", 124 | "sidebar_highlight": "hsla(259, 76%, 58%, 1)", 125 | "sidebar_highlight_text": "hsla(0, 0%, 100%, 1)", 126 | "focus_outline": "hsla(265, 100%, 72%, 1)" 127 | } 128 | }, 129 | 130 | "theme_experiment": { 131 | "colors": { 132 | "focus_outline": "--focus-outline-color" 133 | }, 134 | "properties": { 135 | "zap_gradient": "--panel-separator-zap-gradient" 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /resource/dark-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "browser_specific_settings": { 5 | "gecko": { 6 | "id": "firefox-compact-dark@mozilla.org" 7 | } 8 | }, 9 | 10 | "name": "Dark", 11 | "description": "A theme with a dark color scheme.", 12 | "author": "Mozilla", 13 | "version": "1.3.3", 14 | 15 | "icons": { "32": "icon.svg" }, 16 | 17 | "theme": { 18 | "colors": { 19 | "tab_background_text": "#fbfbfe", 20 | "tab_selected": "rgba(106,106,120,0.7)", 21 | "tab_text": "rgb(255,255,255)", 22 | "icons": "rgb(251,251,254)", 23 | "frame": "rgb(28, 27, 34)", 24 | "frame_inactive": "rgb(31, 30, 37)", 25 | "popup": "rgb(66,65,77)", 26 | "popup_text": "rgb(251,251,254)", 27 | "popup_border": "rgb(82,82,94)", 28 | "popup_highlight": "rgb(43,42,51)", 29 | "tab_line": "transparent", 30 | "toolbar": "rgb(43,42,51)", 31 | "toolbar_top_separator": "transparent", 32 | "toolbar_bottom_separator": "rgba(251, 251, 254, 0.10)", 33 | "toolbar_field": "rgba(0, 0, 0, .3)", 34 | "toolbar_field_border": "transparent", 35 | "toolbar_field_text": "rgb(251,251,254)", 36 | "toolbar_field_focus": "rgb(66,65,77)", 37 | "toolbar_text": "rgb(251, 251, 254)", 38 | "ntp_background": "rgb(43, 42, 51)", 39 | "ntp_card_background": "rgb(66,65,77)", 40 | "ntp_text": "rgb(251, 251, 254)", 41 | "sidebar": "rgb(28, 27, 34)", 42 | "sidebar_text": "rgb(249, 249, 250)", 43 | "sidebar_border": "rgba(251, 251, 254, 0.10)", 44 | "button": "rgba(0, 0, 0, .33)", 45 | "button_hover": "rgba(207, 207, 216, .20)", 46 | "button_active": "rgba(207, 207, 216, .40)", 47 | "button_primary": "rgb(0, 221, 255)", 48 | "button_primary_hover": "rgb(128, 235, 255)", 49 | "button_primary_active": "rgb(170, 242, 255)", 50 | "button_primary_color": "rgb(43, 42, 51)", 51 | "input_background": "#42414D", 52 | "input_color": "rgb(251,251,254)", 53 | "urlbar_popup_separator": "rgb(82,82,94)" 54 | }, 55 | "properties": { 56 | "color_scheme": "dark", 57 | "panel_active": "color-mix(in srgb, currentColor 14%, transparent)", 58 | "toolbar_field_icon_opacity": "1", 59 | "zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)" 60 | } 61 | }, 62 | 63 | "theme_experiment": { 64 | "stylesheet": "experiment.css", 65 | "colors": { 66 | "button": "--button-background-color", 67 | "button_hover": "--button-background-color-hover", 68 | "button_active": "--button-background-color-active", 69 | "button_primary": "--color-accent-primary", 70 | "button_primary_hover": "--color-accent-primary-hover", 71 | "button_primary_active": "--color-accent-primary-active", 72 | "button_primary_color": "--button-text-color-primary", 73 | "input_background": "--input-bgcolor", 74 | "input_color": "--input-color", 75 | "urlbar_popup_separator": "--urlbarView-separator-color", 76 | "zoom_controls": "--zoom-controls-bgcolor" 77 | }, 78 | "properties": { 79 | "panel_active": "--arrowpanel-dimmed-further", 80 | "toolbar_field_icon_opacity": "--urlbar-icon-fill-opacity", 81 | "zap_gradient": "--panel-separator-zap-gradient" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /resource/light-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "browser_specific_settings": { 5 | "gecko": { 6 | "id": "firefox-compact-light@mozilla.org" 7 | } 8 | }, 9 | 10 | "name": "Light", 11 | "description": "A theme with a light color scheme.", 12 | "author": "Mozilla", 13 | "version": "1.3.2", 14 | 15 | "icons": { "32": "icon.svg" }, 16 | 17 | "theme": { 18 | "colors": { 19 | "tab_background_text": "rgb(21,20,26)", 20 | "tab_selected": "#fff", 21 | "tab_text": "rgb(21,20,26)", 22 | "icons": "rgb(91,91,102)", 23 | "frame": "rgb(234, 234, 237)", 24 | "frame_inactive": "rgb(240, 240, 244)", 25 | "popup": "#fff", 26 | "popup_text": "rgb(21,20,26)", 27 | "popup_border": "rgb(240,240,244)", 28 | "popup_highlight": "#e0e0e6", 29 | "popup_highlight_text": "#15141a", 30 | "sidebar": "rgb(255, 255, 255)", 31 | "sidebar_text": "rgb(21, 20, 26)", 32 | "sidebar_border": "rgba(21, 20, 26, 0.1)", 33 | "tab_line": "transparent", 34 | "toolbar": "#f9f9fb", 35 | "toolbar_top_separator": "transparent", 36 | "toolbar_bottom_separator": "rgba(21, 20, 26, 0.1)", 37 | "toolbar_field": "rgba(0, 0, 0, .05)", 38 | "toolbar_field_text": "rgb(21, 20, 26)", 39 | "toolbar_field_border": "transparent", 40 | "toolbar_field_focus": "white", 41 | "toolbar_text": "rgb(21,20,26)", 42 | "ntp_background": "#F9F9FB", 43 | "ntp_text": "rgb(21, 20, 26)", 44 | "popup_action_color": "rgb(91,91,102)", 45 | "button": "rgba(207,207,216,.33)", 46 | "button_hover": "rgba(207,207,216,.66)", 47 | "button_active": "rgb(207,207,216)", 48 | "button_primary": "rgb(0, 97, 224)", 49 | "button_primary_hover": "rgb(2, 80, 187)", 50 | "button_primary_active": "rgb(5, 62, 148)", 51 | "button_primary_color": "rgb(251, 251, 254)", 52 | "input_color": "rgb(21,20,26)", 53 | "input_background": "rgb(255,255,255)", 54 | "urlbar_popup_hover": "rgb(240,240,244)", 55 | "urlbar_popup_separator": "rgb(240,240,244)" 56 | }, 57 | "properties": { 58 | "color_scheme": "light", 59 | "toolbar_field_icon_opacity": "0.72", 60 | "zap_gradient": "linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%)" 61 | } 62 | }, 63 | 64 | "theme_experiment": { 65 | "stylesheet": "experiment.css", 66 | "colors": { 67 | "popup_action_color": "--urlbarView-action-color", 68 | "button": "--button-background-color", 69 | "button_hover": "--button-background-color-hover", 70 | "button_active": "--button-background-color-active", 71 | "button_primary": "--color-accent-primary", 72 | "button_primary_hover": "--color-accent-primary-hover", 73 | "button_primary_active": "--color-accent-primary-active", 74 | "button_primary_color": "--button-text-color-primary", 75 | "input_background": "--input-bgcolor", 76 | "input_color": "--input-color", 77 | "urlbar_popup_hover": "--urlbarView-hover-background", 78 | "urlbar_popup_separator": "--urlbarView-separator-color" 79 | }, 80 | "properties": { 81 | "toolbar_field_icon_opacity": "--urlbar-icon-fill-opacity", 82 | "zap_gradient": "--panel-separator-zap-gradient" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * common.js 3 | */ 4 | 5 | /* constants */ 6 | const TYPE_FROM = 8; 7 | const TYPE_TO = -1; 8 | 9 | /** 10 | * throw error 11 | * @param {!object} e - Error 12 | * @throws - Error 13 | */ 14 | export const throwErr = e => { 15 | throw e; 16 | }; 17 | 18 | /** 19 | * log error 20 | * @param {!object} e - Error 21 | * @returns {boolean} - false 22 | */ 23 | export const logErr = e => { 24 | console.error(e); 25 | return false; 26 | }; 27 | 28 | /** 29 | * log warn 30 | * @param {*} msg - message 31 | * @returns {boolean} - false 32 | */ 33 | export const logWarn = msg => { 34 | msg && console.warn(msg); 35 | return false; 36 | }; 37 | 38 | /** 39 | * log message 40 | * @param {*} msg - message 41 | * @returns {*} - message 42 | */ 43 | export const logMsg = msg => { 44 | msg && console.log(msg); 45 | return msg; 46 | }; 47 | 48 | /** 49 | * get type 50 | * @param {*} o - object to check 51 | * @returns {string} - type of object 52 | */ 53 | export const getType = o => 54 | Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO); 55 | 56 | /** 57 | * is string 58 | * @param {*} o - object to check 59 | * @returns {boolean} - result 60 | */ 61 | export const isString = o => typeof o === 'string' || o instanceof String; 62 | -------------------------------------------------------------------------------- /scripts/file-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file-util.js 3 | */ 4 | 5 | /* api */ 6 | import fs, { promises as fsPromise } from 'node:fs'; 7 | import path from 'node:path'; 8 | import { getType, isString } from './common.js'; 9 | 10 | /* constants */ 11 | const CHAR = 'utf8'; 12 | const PERM_FILE = 0o644; 13 | 14 | /** 15 | * get stat 16 | * @param {string} file - file path 17 | * @returns {object} - file stat 18 | */ 19 | export const getStat = file => 20 | isString(file) && fs.existsSync(file) ? fs.statSync(file) : null; 21 | 22 | /** 23 | * the directory is a directory 24 | * @param {string} dir - directory path 25 | * @returns {boolean} - result 26 | */ 27 | export const isDir = dir => { 28 | const stat = getStat(dir); 29 | return stat ? stat.isDirectory() : false; 30 | }; 31 | 32 | /** 33 | * the file is a file 34 | * @param {string} file - file path 35 | * @returns {boolean} - result 36 | */ 37 | export const isFile = file => { 38 | const stat = getStat(file); 39 | return stat ? stat.isFile() : false; 40 | }; 41 | 42 | /** 43 | * remove the directory and it's files synchronously 44 | * @param {string} dir - directory path 45 | * @returns {void} 46 | */ 47 | export const removeDir = dir => { 48 | if (!isDir(dir)) { 49 | throw new Error(`No such directory: ${dir}`); 50 | } 51 | fs.rmSync(dir, { 52 | force: true, 53 | recursive: true 54 | }); 55 | }; 56 | 57 | /** 58 | * read a file 59 | * @param {string} file - file path 60 | * @param {object} [opt] - options 61 | * @param {string} [opt.encoding] - encoding 62 | * @param {string} [opt.flag] - flag 63 | * @returns {Promise.} - file content 64 | */ 65 | export const readFile = async (file, opt = { encoding: null, flag: 'r' }) => { 66 | if (!isFile(file)) { 67 | throw new Error(`${file} is not a file.`); 68 | } 69 | const value = await fsPromise.readFile(file, opt); 70 | return value; 71 | }; 72 | 73 | /** 74 | * create a file 75 | * @param {string} file - file path to create 76 | * @param {string} value - value to write 77 | * @returns {Promise.} - file path 78 | */ 79 | export const createFile = async (file, value) => { 80 | if (!isString(file)) { 81 | throw new TypeError(`Expected String but got ${getType(file)}.`); 82 | } 83 | if (!isString(value)) { 84 | throw new TypeError(`Expected String but got ${getType(value)}.`); 85 | } 86 | const filePath = path.resolve(file); 87 | await fsPromise.writeFile(filePath, value, { 88 | encoding: CHAR, flag: 'w', mode: PERM_FILE 89 | }); 90 | return filePath; 91 | }; 92 | 93 | /** 94 | * fetch text 95 | * @param {string} url - URL 96 | * @returns {Promise.} - content text 97 | */ 98 | export const fetchText = async url => { 99 | if (!isString(url)) { 100 | throw new TypeError(`Expected String but got ${getType(url)}.`); 101 | } 102 | const res = await fetch(url); 103 | const { ok, status } = res; 104 | if (!ok) { 105 | const msg = `Network response was not ok. status: ${status} url: ${url}`; 106 | throw new Error(msg); 107 | } 108 | return res.text(); 109 | }; 110 | -------------------------------------------------------------------------------- /src/_locales/en_CA/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionLocale": { 3 | "message": "en-CA" 4 | }, 5 | "optionsNarrowTabGroupColorBar": { 6 | "message": "Reduce tab group colour bar width" 7 | }, 8 | "optionsNarrowTabGroupColorBarDetail": { 9 | "message": "Reduce the width of the tab group colour bar." 10 | }, 11 | "optionsReadBrowserSettingsDetail": { 12 | "message": "Adds permissions that allow the extension to read and modify certain values from browser settings. It is recommended to enable this option for better compatibility with built-in browser behaviours." 13 | }, 14 | "optionsUseFrameColor": { 15 | "message": "Use tab colour for sidebar" 16 | }, 17 | "optionsUseFrameColorDetail": { 18 | "message": "Override the background colour of the sidebar with the background colour of the inactive tab." 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/_locales/en_GB/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionLocale": { 3 | "message": "en-GB" 4 | }, 5 | "optionsNarrowTabGroupColorBar": { 6 | "message": "Reduce tab group colour bar width" 7 | }, 8 | "optionsNarrowTabGroupColorBarDetail": { 9 | "message": "Reduce the width of the tab group colour bar." 10 | }, 11 | "optionsReadBrowserSettingsDetail": { 12 | "message": "Adds permissions that allow the extension to read and modify certain values from browser settings. It is recommended to enable this option for better compatibility with built-in browser behaviours." 13 | }, 14 | "optionsUseFrameColor": { 15 | "message": "Use tab colour for sidebar" 16 | }, 17 | "optionsUseFrameColorDetail": { 18 | "message": "Override the background colour of the sidebar with the background colour of the inactive tab." 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/css/options.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * options.css 5 | */ 6 | 7 | :root { 8 | --box-background-color: rgb(240 240 240); 9 | --box-border-color: rgb(192 192 192); 10 | --box-border-color-alpha: rgb(192 192 192 / 50%); 11 | --box-color: rgb(16 16 16); 12 | --info-background-color: #008ea4; 13 | --info-color: #fff; 14 | --page-background-color: rgb(250 250 250); 15 | --page-color: #000; 16 | --page-border-color: rgb(128 128 128); 17 | --warn-background-color: transparent; 18 | --warn-color: #d70022; 19 | 20 | color-scheme: normal; 21 | } 22 | 23 | html, body, body * { 24 | box-sizing: content-box; 25 | } 26 | 27 | body { 28 | padding: 1rem 1rem 0; 29 | background-color: var(--page-background-color); 30 | color: var(--page-color); 31 | line-height: 1.5; 32 | } 33 | 34 | main { 35 | margin: 1rem 0; 36 | } 37 | 38 | footer { 39 | margin: 1rem 0 2rem; 40 | border-top: 1px solid var(--page-border-color); 41 | } 42 | 43 | section { 44 | margin: 1.5rem 0; 45 | } 46 | 47 | section + section { 48 | border-top: 1px dotted var(--page-border-color); 49 | } 50 | 51 | section section + section { 52 | border-top: none; 53 | } 54 | 55 | main > section + p { 56 | margin-top: 1rem; 57 | border-top: 1px dotted var(--page-border-color); 58 | padding-top: 1rem; 59 | } 60 | 61 | h1 { 62 | margin: 1rem 0; 63 | font-size: 1.2rem; 64 | } 65 | 66 | header > h1 { 67 | margin-top: 0; 68 | } 69 | 70 | header > h1::before { 71 | display: inline-block; 72 | margin-right: 0.5rem; 73 | width: 1.44em; 74 | height: 1.44em; 75 | background-image: url("../img/icon.svg"); 76 | background-size: 1.44em 1.44em; 77 | content: ""; 78 | vertical-align: top; 79 | } 80 | 81 | form { 82 | margin: 0 0 2rem; 83 | } 84 | 85 | fieldset { 86 | margin-top: 1rem; 87 | border: 1px solid var(--page-border-color); 88 | border-radius: 8px; 89 | } 90 | 91 | legend { 92 | margin: 1rem 0.5rem; 93 | font-weight: bold; 94 | } 95 | 96 | fieldset > div { 97 | display: flex; 98 | flex-flow: row wrap; 99 | margin: 0; 100 | vertical-align: middle; 101 | } 102 | 103 | fieldset > div > p { 104 | flex: 1 1 calc(100% / 13 * 8); 105 | margin: 0.5rem 0; 106 | min-width: 15rem; 107 | } 108 | 109 | details { 110 | position: relative; 111 | flex: 1 1 calc(100% / 13 * 5); 112 | margin: 0.5rem 0; 113 | font-size: smaller; 114 | } 115 | 116 | details > div { 117 | position: absolute; 118 | left: 0.9rem; 119 | border: 1px solid var(--box-border-color); 120 | border-radius: 4px; 121 | background-color: var(--box-background-color); 122 | color: var(--box-color); 123 | z-index: 2; 124 | } 125 | 126 | fieldset:last-child > div:last-child > details > div { 127 | bottom: 1.5rem; 128 | } 129 | 130 | details > div > p { 131 | margin: 0.5rem; 132 | } 133 | 134 | label + input[type="radio"] { 135 | margin-left: 0.5rem; 136 | } 137 | 138 | input[type="text"], 139 | input[type="url"] { 140 | min-width: 15rem; 141 | max-width: calc(100% - 1rem); 142 | } 143 | 144 | textarea { 145 | min-width: 15rem; 146 | max-width: calc(100% - 1rem); 147 | vertical-align: top; 148 | } 149 | 150 | input[type="submit"], 151 | button { 152 | font-size: 0.9rem; 153 | } 154 | 155 | img { 156 | position: absolute; 157 | inset: 0; 158 | margin: auto; 159 | width: 1.728rem; 160 | height: 1.728rem; 161 | } 162 | 163 | figure { 164 | margin: 1rem; 165 | } 166 | 167 | pre { 168 | border: 1px solid var(--page-border-color); 169 | border-radius: 8px; 170 | overflow-x: auto; 171 | } 172 | 173 | code { 174 | display: inline-block; 175 | } 176 | 177 | pre > code { 178 | margin: 0 1rem; 179 | } 180 | 181 | .label { 182 | box-sizing: border-box; 183 | display: inline-block; 184 | padding-left: 0.2rem; 185 | min-width: 15rem; 186 | } 187 | 188 | .sub-item { 189 | display: block; 190 | margin-left: 1.5rem; 191 | } 192 | 193 | .sub-item > .label { 194 | min-width: 13.5rem; 195 | } 196 | 197 | .status { 198 | max-width: calc(100% - 1rem); 199 | } 200 | 201 | .info { 202 | border-radius: 4px; 203 | padding: 0.2em 0.5em; 204 | background-color: var(--info-background-color); 205 | color: var(--info-color); 206 | } 207 | 208 | .warn { 209 | background-color: var(--warn-background-color); 210 | color: var(--warn-color); 211 | } 212 | 213 | #customThemeSettings[hidden] { 214 | display: none; 215 | } 216 | 217 | #customThemeSettings { 218 | display: table; 219 | margin: 0.5rem; 220 | border-collapse: collapse; 221 | table-layout: fixed; 222 | min-width: 15rem; 223 | width: calc(100% / 13 * 8 - 1rem); 224 | font-size: smaller; 225 | } 226 | 227 | #customThemeSettings caption { 228 | padding: 0.25rem; 229 | } 230 | 231 | #customThemeSettings th, 232 | #customThemeSettings td { 233 | padding: 0.25rem; 234 | text-align: center; 235 | vertical-align: middle; 236 | } 237 | 238 | /* zoom slider */ 239 | #customThemeSettings tr:has(#customZoom) { 240 | display: none; 241 | } 242 | 243 | @supports (zoom: 1) { 244 | #customThemeSettings tr:has(#customZoom) { 245 | border-top: double; 246 | display: table-row; 247 | } 248 | 249 | span:has(> #customZoom) { 250 | display: inline-block; 251 | } 252 | 253 | #customZoom + datalist { 254 | display: flex; 255 | flex-direction: row; 256 | justify-content: space-between; 257 | line-height: 0.5; 258 | } 259 | 260 | #customZoom + datalist > option { 261 | margin: 0 0.25em; 262 | padding: 0; 263 | font-family: monospace; 264 | } 265 | } 266 | 267 | #userCSS { 268 | display: block; 269 | } 270 | 271 | #userCSS[hidden] { 272 | display: none; 273 | } 274 | 275 | @media (prefers-color-scheme: dark) { 276 | :root { 277 | --box-background-color: rgb(35 34 43); 278 | --box-border-color: rgb(64 64 64); 279 | --box-border-color-alpha: rgb(64 64 64 / 50%); 280 | --box-color: rgb(240 240 240); 281 | --info-background-color: #008ea4; 282 | --info-color: #fff; 283 | --page-background-color: rgb(28 27 34); 284 | --page-color: rgb(251 251 254); 285 | --page-border-color: rgb(128 128 128); 286 | --warn-background-color: transparent; 287 | --warn-color: #ff4f5e; 288 | 289 | color-scheme: dark; 290 | } 291 | 292 | header > h1::before { 293 | background-image: url("../img/icon.svg#light"); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/html/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sidebar Tabs 6 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 76 |
77 | 78 | 99 | 100 | 112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /src/img/addons-favicon.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Addons Icon 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/img/audio-muted.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Audio Muted Icon 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/audio-play.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Audio Playing Icon 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/img/briefcase.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Briefcase Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/img/cart.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Cart Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/img/chill.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Chill Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/img/circle.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Circle Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/img/close.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Close Icon 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/img/customize-favicon.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Customize Icon 12 | 13 | 14 | 15 | 16 | 27 | 42 | 43 | -------------------------------------------------------------------------------- /src/img/default-favicon.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Default Favicon 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/img/dollar.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Dollar Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/img/drop-arrow.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Drop Arrow Icon 12 | 13 | 14 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /src/img/edit.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Edit Icon 12 | 13 | 14 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /src/img/fence.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Fence Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/img/fingerprint.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Fingerprint Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/img/folder.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Folder Icon 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/img/food.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Food Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/img/fruit.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Fruit Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/img/gift.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Gift Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/img/hourglass.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Close Icon 12 | 13 | 14 | 15 | 16 | 17 | 58 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/img/icon.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | 12 | 13 | 14 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/img/loading.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Loading Throbber Icon 12 | 13 | 14 | 15 | 16 | 17 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/img/new-tab.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | New Tab Icon 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/img/options-favicon.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Options Icon 12 | 13 | 14 | 15 | 16 | 29 | 30 | -------------------------------------------------------------------------------- /src/img/pet.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Pet Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/img/pin.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Pin Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 25 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/img/spacer.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/img/tree.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Tree Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/img/twitter-logo-blue.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Twitter Logo Blue 12 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | Twitter_Logo_Blue 27 | 28 | 39 | 40 | -------------------------------------------------------------------------------- /src/img/vacation.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Vacation Icon 12 | 13 | 14 | 15 | 16 | 22 | 23 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/lib/color/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 asamuzaK (Kazz) 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 | -------------------------------------------------------------------------------- /src/lib/color/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@asamuzakjp/css-color", 3 | "description": "CSS color - Resolve and convert CSS colors.", 4 | "author": "asamuzaK", 5 | "license": "MIT", 6 | "homepage": "https://github.com/asamuzaK/cssColor#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/asamuzaK/cssColor.git" 10 | }, 11 | "type": "module", 12 | "version": "3.2.0", 13 | "origins": [ 14 | { 15 | "file": "LICENSE", 16 | "raw": "https://raw.githubusercontent.com/asamuzaK/cssColor/v3.2.0/LICENSE", 17 | "cdn": "https://unpkg.com/@asamuzakjp/css-color@3.2.0/LICENSE" 18 | }, 19 | { 20 | "file": "css-color.min.js", 21 | "raw": "https://raw.githubusercontent.com/asamuzaK/cssColor/v3.2.0/dist/browser/css-color.min.js", 22 | "cdn": "https://unpkg.com/@asamuzakjp/css-color@3.2.0/dist/browser/css-color.min.js" 23 | }, 24 | { 25 | "file": "css-color.min.js.map", 26 | "raw": "https://raw.githubusercontent.com/asamuzaK/cssColor/v3.2.0/dist/browser/css-color.min.js.map", 27 | "cdn": "https://unpkg.com/@asamuzakjp/css-color@3.2.0/dist/browser/css-color.min.js.map" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/purify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dompurify", 3 | "description": "DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It's written in JavaScript and works in all modern browsers (Safari, Opera (15+), Internet Explorer (10+), Firefox and Chrome - as well as almost anything else using Blink or WebKit). DOMPurify is written by security people who have vast background in web attacks and XSS. Fear not.", 4 | "author": "Dr.-Ing. Mario Heiderich, Cure53 (https://cure53.de/)", 5 | "license": "(MPL-2.0 OR Apache-2.0)", 6 | "homepage": "https://github.com/cure53/DOMPurify", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/cure53/DOMPurify.git" 10 | }, 11 | "type": "commonjs", 12 | "version": "3.2.6", 13 | "origins": [ 14 | { 15 | "file": "LICENSE", 16 | "raw": "https://raw.githubusercontent.com/cure53/DOMPurify/3.2.6/LICENSE", 17 | "cdn": "https://unpkg.com/dompurify@3.2.6/LICENSE" 18 | }, 19 | { 20 | "file": "purify.min.js", 21 | "raw": "https://raw.githubusercontent.com/cure53/DOMPurify/3.2.6/dist/purify.min.js", 22 | "cdn": "https://unpkg.com/dompurify@3.2.6/dist/purify.min.js" 23 | }, 24 | { 25 | "file": "purify.min.js.map", 26 | "raw": "https://raw.githubusercontent.com/cure53/DOMPurify/3.2.6/dist/purify.min.js.map", 27 | "cdn": "https://unpkg.com/dompurify@3.2.6/dist/purify.min.js.map" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/tldts/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Thomas Parisot, 2018 Rémi Berson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 6 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 7 | subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 13 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/lib/tldts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tldts-experimental", 3 | "description": "Library to work against complex domain names, subdomains and URIs.", 4 | "author": { 5 | "name": "Rémi Berson" 6 | }, 7 | "license": "MIT", 8 | "homepage": "https://github.com/remusao/tldts#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/remusao/tldts.git" 12 | }, 13 | "type": "module", 14 | "version": "7.0.7", 15 | "origins": [ 16 | { 17 | "file": "LICENSE", 18 | "cdn": "https://unpkg.com/tldts-experimental@7.0.7/LICENSE" 19 | }, 20 | { 21 | "file": "index.esm.min.js", 22 | "cdn": "https://unpkg.com/tldts-experimental@7.0.7/dist/index.esm.min.js" 23 | }, 24 | { 25 | "file": "index.esm.min.js.map", 26 | "cdn": "https://unpkg.com/tldts-experimental@7.0.7/dist/index.esm.min.js.map" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/url/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 asamuzaK (Kazz) 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 | -------------------------------------------------------------------------------- /src/lib/url/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-sanitizer", 3 | "description": "URL sanitizer for Node.js, browsers and web sites.", 4 | "author": "asamuzaK", 5 | "license": "MIT", 6 | "homepage": "https://github.com/asamuzaK/urlSanitizer", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/asamuzaK/urlSanitizer.git" 10 | }, 11 | "type": "module", 12 | "version": "2.0.9", 13 | "origins": [ 14 | { 15 | "file": "LICENSE", 16 | "raw": "https://raw.githubusercontent.com/asamuzaK/urlSanitizer/v2.0.9/LICENSE", 17 | "cdn": "https://unpkg.com/url-sanitizer@2.0.9/LICENSE" 18 | }, 19 | { 20 | "file": "url-sanitizer-wo-dompurify.min.js", 21 | "raw": "https://raw.githubusercontent.com/asamuzaK/urlSanitizer/v2.0.9/dist/url-sanitizer-wo-dompurify.min.js", 22 | "cdn": "https://unpkg.com/url-sanitizer@2.0.9/dist/url-sanitizer-wo-dompurify.min.js" 23 | }, 24 | { 25 | "file": "url-sanitizer-wo-dompurify.min.js.map", 26 | "raw": "https://raw.githubusercontent.com/asamuzaK/urlSanitizer/v2.0.9/dist/url-sanitizer-wo-dompurify.min.js.map", 27 | "cdn": "https://unpkg.com/url-sanitizer@2.0.9/dist/url-sanitizer-wo-dompurify.min.js.map" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "default_icon": "img/icon.svg#current", 4 | "default_title": "__MSG_extensionName__" 5 | }, 6 | "background": { 7 | "scripts": [ 8 | "mjs/background.js" 9 | ], 10 | "type": "module" 11 | }, 12 | "browser_specific_settings": { 13 | "gecko": { 14 | "id": "sidebarTabs@asamuzak.jp", 15 | "strict_min_version": "115.0" 16 | } 17 | }, 18 | "commands": { 19 | "toggleOpenCloseState": { 20 | "description": "__MSG_toggleOpenCloseState__" 21 | } 22 | }, 23 | "default_locale": "en", 24 | "description": "__MSG_extensionDescription__", 25 | "homepage_url": "https://github.com/asamuzaK/sidebarTabs", 26 | "icons": { 27 | "16": "img/icon.svg#current", 28 | "32": "img/icon.svg#current", 29 | "64": "img/icon.svg" 30 | }, 31 | "manifest_version": 3, 32 | "name": "__MSG_extensionName__", 33 | "optional_permissions": [ 34 | "browserSettings" 35 | ], 36 | "options_ui": { 37 | "open_in_tab": true, 38 | "page": "html/options.html" 39 | }, 40 | "permissions": [ 41 | "activeTab", 42 | "bookmarks", 43 | "contextualIdentities", 44 | "cookies", 45 | "management", 46 | "menus", 47 | "menus.overrideContext", 48 | "search", 49 | "sessions", 50 | "storage", 51 | "tabs", 52 | "theme" 53 | ], 54 | "short_name": "__MSG_extensionShortName__", 55 | "sidebar_action": { 56 | "default_icon": "img/icon.svg#current", 57 | "default_panel": "html/sidebar.html", 58 | "default_title": "__MSG_extensionShortName__" 59 | }, 60 | "version": "16.0.4" 61 | } 62 | -------------------------------------------------------------------------------- /src/mjs/background-main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * background-main.js 3 | */ 4 | 5 | /* shared */ 6 | import { getCurrentWindow, getWindow } from './browser.js'; 7 | import { 8 | getType, isObjectNotEmpty, isString, logErr, throwErr 9 | } from './common.js'; 10 | import { ports, removePort } from './port.js'; 11 | import { restoreContextMenu } from './menu.js'; 12 | import { saveSessionTabList } from './session.js'; 13 | import { 14 | SESSION_SAVE, SIDEBAR, SIDEBAR_STATE_UPDATE, TOGGLE_STATE 15 | } from './constant.js'; 16 | 17 | /* api */ 18 | const { sidebarAction, windows } = browser; 19 | 20 | /* constants */ 21 | const { WINDOW_ID_CURRENT, WINDOW_ID_NONE } = windows; 22 | const REG_PORT = new RegExp(`${SIDEBAR}_(-?\\d+)`); 23 | 24 | /* sidebar */ 25 | export const sidebar = new Map(); 26 | 27 | /** 28 | * set sidebar state 29 | * @param {number} [windowId] - window ID 30 | * @returns {Promise.} - void 31 | */ 32 | export const setSidebarState = async windowId => { 33 | let win; 34 | if (!Number.isInteger(windowId) || windowId === WINDOW_ID_CURRENT) { 35 | win = await getCurrentWindow(); 36 | windowId = win.id; 37 | } else if (windowId !== WINDOW_ID_NONE) { 38 | win = await getWindow(windowId); 39 | } 40 | if (win) { 41 | const { incognito, sessionId, type } = win; 42 | if (type === 'normal') { 43 | const isOpen = await sidebarAction.isOpen({ windowId }); 44 | let value; 45 | if (sidebar.has(windowId)) { 46 | value = sidebar.get(windowId); 47 | value.incognito = incognito; 48 | value.isOpen = isOpen; 49 | value.sessionId = sessionId; 50 | value.windowId = windowId; 51 | } else { 52 | value = { 53 | incognito, 54 | isOpen, 55 | sessionId, 56 | windowId, 57 | remove: false, 58 | sessionValue: null 59 | }; 60 | } 61 | sidebar.set(windowId, value); 62 | } 63 | } 64 | }; 65 | 66 | /** 67 | * remove sidebar state 68 | * @param {number} windowId - window ID 69 | * @returns {Promise.} - result 70 | */ 71 | export const removeSidebarState = async windowId => { 72 | const res = sidebar.delete(windowId); 73 | return res; 74 | }; 75 | 76 | /** 77 | * toggle sidebar 78 | * @returns {Promise} - sidebarAction.toggle() 79 | */ 80 | export const toggleSidebar = async () => sidebarAction.toggle(); 81 | 82 | /** 83 | * handle save session request 84 | * @param {string} domString - DOM string 85 | * @param {number} windowId - window ID 86 | * @returns {Promise.} - result 87 | */ 88 | export const handleSaveSessionRequest = async (domString, windowId) => { 89 | if (!isString(domString)) { 90 | throw new TypeError(`Expected String but got ${getType(domString)}.`); 91 | } 92 | if (!Number.isInteger(windowId)) { 93 | throw new TypeError(`Expected Number but got ${getType(windowId)}.`); 94 | } 95 | const portId = `${SIDEBAR}_${windowId}`; 96 | let res; 97 | if (ports.has(portId) && sidebar.has(windowId)) { 98 | const value = sidebar.get(windowId); 99 | const { incognito } = value; 100 | if (!incognito) { 101 | value.sessionValue = domString; 102 | sidebar.set(windowId, value); 103 | res = await saveSessionTabList(domString, windowId); 104 | if (res) { 105 | const currentValue = sidebar.get(windowId); 106 | const { remove, sessionValue } = currentValue; 107 | if (sessionValue !== domString) { 108 | res = await handleSaveSessionRequest(sessionValue, windowId); 109 | } 110 | if (res && remove) { 111 | await removeSidebarState(windowId); 112 | } 113 | } 114 | } 115 | } 116 | return !!res; 117 | }; 118 | 119 | /** 120 | * handle runtime message 121 | * @param {object} [msg] - message 122 | * @returns {Promise.} - results of each handler 123 | */ 124 | export const handleMsg = async msg => { 125 | const func = []; 126 | if (isObjectNotEmpty(msg)) { 127 | const items = Object.entries(msg); 128 | for (const [key, value] of items) { 129 | switch (key) { 130 | case SESSION_SAVE: { 131 | const { domString, windowId } = value; 132 | if (isString(domString) && Number.isInteger(windowId)) { 133 | func.push(handleSaveSessionRequest(domString, windowId)); 134 | } 135 | break; 136 | } 137 | case SIDEBAR_STATE_UPDATE: { 138 | const { windowId } = value; 139 | func.push(setSidebarState(windowId)); 140 | break; 141 | } 142 | default: 143 | } 144 | } 145 | } 146 | return Promise.all(func); 147 | }; 148 | 149 | /** 150 | * port on message 151 | * @param {object} msg - message 152 | * @returns {Promise} - promise chain 153 | */ 154 | export const portOnMessage = msg => handleMsg(msg).catch(throwErr); 155 | 156 | /** 157 | * handle disconnected port 158 | * @param {object} [port] - runtime.Port 159 | * @returns {Promise.} - void 160 | */ 161 | export const handleDisconnectedPort = async (port = {}) => { 162 | const { error, name: portId } = port; 163 | if (error) { 164 | logErr(error); 165 | } 166 | if (isString(portId) && REG_PORT.test(portId)) { 167 | const [, winId] = REG_PORT.exec(portId); 168 | const windowId = winId * 1; 169 | if (sidebar.has(windowId)) { 170 | const value = sidebar.get(windowId); 171 | const { incognito, sessionValue } = value; 172 | if (!incognito && isString(sessionValue)) { 173 | value.remove = true; 174 | sidebar.set(windowId, value); 175 | await handleSaveSessionRequest(sessionValue, windowId); 176 | } else { 177 | await removeSidebarState(windowId); 178 | } 179 | } 180 | } 181 | if (ports.has(portId)) { 182 | await removePort(portId); 183 | } 184 | }; 185 | 186 | /** 187 | * port on disconnect 188 | * @param {object} port - runtime.Port 189 | * @returns {Promise} - promise chain 190 | */ 191 | export const portOnDisconnect = port => 192 | handleDisconnectedPort(port).catch(throwErr); 193 | 194 | /** 195 | * handle connected port 196 | * @param {object} [port] - runtime.Port 197 | * @returns {Promise.} - void 198 | */ 199 | export const handleConnectedPort = async (port = {}) => { 200 | const { name: portId } = port; 201 | if (isString(portId) && REG_PORT.test(portId)) { 202 | const [, winId] = REG_PORT.exec(portId); 203 | const windowId = winId * 1; 204 | if (windowId !== WINDOW_ID_NONE) { 205 | port.onDisconnect.addListener(portOnDisconnect); 206 | port.onMessage.addListener(portOnMessage); 207 | ports.set(portId, port); 208 | await setSidebarState(windowId).then(restoreContextMenu); 209 | } 210 | } 211 | }; 212 | 213 | /** 214 | * handle command 215 | * @param {string} cmd - command 216 | * @returns {Promise.} - promise chain 217 | */ 218 | export const handleCmd = async cmd => { 219 | if (!isString(cmd)) { 220 | throw new TypeError(`Expected String but got ${getType(cmd)}.`); 221 | } 222 | let func; 223 | switch (cmd) { 224 | case TOGGLE_STATE: 225 | func = toggleSidebar().then(setSidebarState); 226 | break; 227 | default: 228 | } 229 | return func || null; 230 | }; 231 | 232 | // For test 233 | export { ports }; 234 | -------------------------------------------------------------------------------- /src/mjs/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * background.js 3 | */ 4 | 5 | /* shared */ 6 | import { 7 | handleCmd, handleConnectedPort, handleMsg, removeSidebarState, 8 | setSidebarState, toggleSidebar 9 | } from './background-main.js'; 10 | import { throwErr } from './common.js'; 11 | import { 12 | createContextualIdentitiesMenu, removeContextualIdentitiesMenu, 13 | restoreContextMenu, updateContextualIdentitiesMenu 14 | } from './menu.js'; 15 | 16 | /* api */ 17 | const { action, commands, contextualIdentities, runtime, windows } = browser; 18 | 19 | /* listeners */ 20 | action.onClicked.addListener(() => 21 | toggleSidebar().then(setSidebarState).catch(throwErr) 22 | ); 23 | commands.onCommand.addListener((cmd, tab) => 24 | handleCmd(cmd, tab).catch(throwErr) 25 | ); 26 | contextualIdentities.onCreated.addListener(info => 27 | createContextualIdentitiesMenu(info).catch(throwErr) 28 | ); 29 | contextualIdentities.onRemoved.addListener(info => 30 | removeContextualIdentitiesMenu(info).catch(throwErr) 31 | ); 32 | contextualIdentities.onUpdated.addListener(info => 33 | updateContextualIdentitiesMenu(info).catch(throwErr) 34 | ); 35 | runtime.onConnect.addListener(port => 36 | handleConnectedPort(port).catch(throwErr) 37 | ); 38 | runtime.onInstalled.addListener(() => restoreContextMenu().catch(throwErr)); 39 | runtime.onMessage.addListener((msg, sender) => 40 | handleMsg(msg, sender).catch(throwErr) 41 | ); 42 | runtime.onStartup.addListener(() => restoreContextMenu().catch(throwErr)); 43 | windows.onFocusChanged.addListener(windowId => 44 | setSidebarState(windowId).catch(throwErr) 45 | ); 46 | windows.onRemoved.addListener(windowId => 47 | removeSidebarState(windowId).catch(throwErr) 48 | ); 49 | -------------------------------------------------------------------------------- /src/mjs/bookmark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bookmark.js 3 | */ 4 | 5 | /* shared */ 6 | import { createBookmark, getBookmarkTreeNode, getStorage } from './browser.js'; 7 | import { getType, isObjectNotEmpty, isString, logErr } from './common.js'; 8 | import { BOOKMARK_FOLDER_MSG, BOOKMARK_LOCATION } from './constant.js'; 9 | 10 | /* api */ 11 | const { i18n } = browser; 12 | 13 | /* bookmark folder map */ 14 | export const folderMap = new Map(); 15 | 16 | /** 17 | * create folder map 18 | * @param {string} [node] - bookmark tree node 19 | * @param {boolean} [recurse] - create bookmark folder tree recursively 20 | * @returns {Promise.} - void 21 | */ 22 | export const createFolderMap = async (node, recurse = false) => { 23 | if (isObjectNotEmpty(node)) { 24 | const { children, id, parentId, title, type } = node; 25 | if (id && type === 'folder') { 26 | if (!folderMap.has(id)) { 27 | folderMap.set(id, { 28 | children: new Set(), 29 | id, 30 | parentId, 31 | title, 32 | type 33 | }); 34 | } 35 | if (parentId && folderMap.has(parentId)) { 36 | const parent = folderMap.get(parentId); 37 | parent.children.add(id); 38 | } 39 | if ((!parentId || recurse) && Array.isArray(children)) { 40 | const func = []; 41 | for (const child of children) { 42 | func.push(createFolderMap(child, recurse)); 43 | } 44 | await Promise.all(func); 45 | } 46 | } 47 | } 48 | }; 49 | 50 | /** 51 | * get folder map 52 | * @param {boolean} [recurse] - create bookmark folder tree recursively 53 | * @returns {Promise.} - folderMap 54 | */ 55 | export const getFolderMap = async (recurse = false) => { 56 | const [tree] = await getBookmarkTreeNode(); 57 | folderMap.clear(); 58 | await createFolderMap(tree, recurse); 59 | return folderMap; 60 | }; 61 | 62 | /** 63 | * get bookmark location ID from storage 64 | * @returns {Promise.} - bookmark location ID 65 | */ 66 | export const getBookmarkLocationId = async () => { 67 | const folder = await getStorage(BOOKMARK_LOCATION); 68 | let id; 69 | if (isObjectNotEmpty(folder) && 70 | Object.prototype.hasOwnProperty.call(folder, BOOKMARK_LOCATION)) { 71 | const { value } = folder[BOOKMARK_LOCATION]; 72 | if (value && isString(value)) { 73 | try { 74 | const [tree] = await getBookmarkTreeNode(value); 75 | if (isObjectNotEmpty(tree) && 76 | Object.prototype.hasOwnProperty.call(tree, 'id')) { 77 | const { id: treeId } = tree; 78 | id = treeId; 79 | } 80 | } catch (e) { 81 | id = null; 82 | logErr(e); 83 | } 84 | } 85 | } 86 | return id || null; 87 | }; 88 | 89 | /** 90 | * bookmark tabs 91 | * @param {Array} nodes - array of node 92 | * @param {string} [name] - folder name 93 | * @returns {Promise.} - results of each handler 94 | */ 95 | export const bookmarkTabs = async (nodes, name = '') => { 96 | if (!Array.isArray(nodes)) { 97 | throw new TypeError(`Expected Array but got ${getType(nodes)}.`); 98 | } 99 | let folderId = await getBookmarkLocationId(); 100 | if (nodes.length > 1) { 101 | const msg = i18n.getMessage(BOOKMARK_FOLDER_MSG); 102 | const folderTitle = window.prompt(msg, name); 103 | if (folderTitle) { 104 | const folder = await createBookmark({ 105 | parentId: folderId || undefined, 106 | title: folderTitle, 107 | type: 'folder' 108 | }); 109 | if (isObjectNotEmpty(folder) && 110 | Object.prototype.hasOwnProperty.call(folder, 'id')) { 111 | const { id } = folder; 112 | folderId = id; 113 | } 114 | } 115 | } 116 | const func = []; 117 | for (const item of nodes) { 118 | if (item.nodeType === Node.ELEMENT_NODE) { 119 | const { dataset } = item; 120 | const itemTab = dataset.tab && JSON.parse(dataset.tab); 121 | if (itemTab) { 122 | const { title, url } = itemTab; 123 | func.push(createBookmark({ 124 | parentId: folderId || undefined, 125 | title, 126 | url 127 | })); 128 | } 129 | } 130 | } 131 | return Promise.all(func); 132 | }; 133 | -------------------------------------------------------------------------------- /src/mjs/color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * color.js 3 | */ 4 | 5 | /* shared */ 6 | import { convert, resolve } from '../lib/color/css-color.min.js'; 7 | import { getType } from './common.js'; 8 | 9 | const { colorToHex, colorToRgb, numberToHex } = convert; 10 | 11 | /* constant */ 12 | const MAX_RGB = 255; 13 | 14 | /** 15 | * convert rgb to hex color 16 | * @param {Array.} rgb - [r, g, b, a] r|g|b: 0..255 a: 0..1|undefined 17 | * @returns {string} - hex color; 18 | */ 19 | export const convertRgbToHex = rgb => { 20 | if (!Array.isArray(rgb)) { 21 | throw new TypeError(`Expected Array but got ${getType(rgb)}.`); 22 | } 23 | const [r, g, b, a] = rgb; 24 | const res = colorToHex(`rgb(${r} ${g} ${b} / ${isNaN(a) ? 1 : a})`, { 25 | alpha: true 26 | }); 27 | return res; 28 | }; 29 | 30 | /** 31 | * get color in hex color notation 32 | * @param {string} value - value 33 | * @param {object} [opt] - options 34 | * @returns {?string|Array} - hex color or array of [property, hex] pair 35 | */ 36 | export const getColorInHex = (value, opt = {}) => { 37 | const { alpha, currentColor, property } = opt; 38 | const format = alpha ? 'hexAlpha' : 'hex'; 39 | const hex = resolve(value, { 40 | currentColor, 41 | format 42 | }); 43 | if (property) { 44 | return [property, hex]; 45 | } 46 | return hex; 47 | }; 48 | 49 | /** 50 | * composite two layered colors 51 | * @param {string} overlay - overlay color 52 | * @param {string} base - base color 53 | * @returns {string} - hex color 54 | */ 55 | export const compositeLayeredColors = (overlay, base) => { 56 | const [rO, gO, bO, aO] = colorToRgb(overlay); 57 | const [rB, gB, bB, aB] = colorToRgb(base); 58 | const alpha = 1 - (1 - aO) * (1 - aB); 59 | let hex; 60 | if (aO === 1) { 61 | hex = colorToHex(`rgb(${rO} ${gO} ${bO})`); 62 | } else if (aO === 0) { 63 | hex = colorToHex(`rgb(${rB} ${gB} ${bB} / ${aB})`, { 64 | alpha: true 65 | }); 66 | } else if (alpha) { 67 | const alphaO = aO / alpha; 68 | const alphaB = aB * (1 - aO) / alpha; 69 | const r = numberToHex(rO * alphaO + rB * alphaB); 70 | const g = numberToHex(gO * alphaO + gB * alphaB); 71 | const b = numberToHex(bO * alphaO + bB * alphaB); 72 | const a = numberToHex(alpha * MAX_RGB); 73 | if (a === 'ff') { 74 | hex = `#${r}${g}${b}`; 75 | } else { 76 | hex = `#${r}${g}${b}${a}`; 77 | } 78 | } 79 | return hex; 80 | }; 81 | -------------------------------------------------------------------------------- /src/mjs/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * common.js 3 | */ 4 | 5 | /* constants */ 6 | const TYPE_FROM = 8; 7 | const TYPE_TO = -1; 8 | 9 | /** 10 | * log error 11 | * @param {!object} e - Error 12 | * @returns {boolean} - false 13 | */ 14 | export const logErr = e => { 15 | if (e?.message) { 16 | console.error(e.message); 17 | } else { 18 | console.error(e); 19 | } 20 | return false; 21 | }; 22 | 23 | /** 24 | * throw error 25 | * @param {!object} e - Error 26 | * @throws 27 | */ 28 | export const throwErr = e => { 29 | logErr(e); 30 | throw e; 31 | }; 32 | 33 | /** 34 | * log warn 35 | * @param {*} [msg] - message 36 | * @returns {boolean} - false 37 | */ 38 | export const logWarn = msg => { 39 | if (msg) { 40 | console.warn(msg); 41 | } 42 | return false; 43 | }; 44 | 45 | /** 46 | * log message 47 | * @param {*} [msg] - message 48 | * @returns {object} - message 49 | */ 50 | export const logMsg = msg => { 51 | if (msg) { 52 | console.log(msg); 53 | } 54 | return msg; 55 | }; 56 | 57 | /** 58 | * get type 59 | * @param {*} o - object to check 60 | * @returns {string} - type of object 61 | */ 62 | export const getType = o => 63 | Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO); 64 | 65 | /** 66 | * is string 67 | * @param {*} o - object to check 68 | * @returns {boolean} - result 69 | */ 70 | export const isString = o => typeof o === 'string' || o instanceof String; 71 | 72 | /** 73 | * is object, and not an empty object 74 | * @param {*} o - object to check; 75 | * @returns {boolean} - result 76 | */ 77 | export const isObjectNotEmpty = o => { 78 | const items = /Object/i.test(getType(o)) && Object.keys(o); 79 | return !!(items?.length); 80 | }; 81 | 82 | /** 83 | * sleep 84 | * @param {number} [msec] - millisecond 85 | * @param {boolean} [doReject] - reject instead of resolve 86 | * @returns {?Promise} - resolve / reject 87 | */ 88 | export const sleep = (msec = 0, doReject = false) => { 89 | let func; 90 | if (Number.isInteger(msec) && msec >= 0) { 91 | func = new Promise((resolve, reject) => { 92 | if (doReject) { 93 | setTimeout(reject, msec); 94 | } else { 95 | setTimeout(resolve, msec); 96 | } 97 | }); 98 | } 99 | return func || null; 100 | }; 101 | 102 | /** 103 | * add contenteditable attribute to element 104 | * @param {object} [elm] - Element 105 | * @param {boolean} [focus] - focus Element 106 | * @returns {object} - Element 107 | */ 108 | export const addElementContentEditable = (elm, focus) => { 109 | if (elm?.nodeType === Node.ELEMENT_NODE) { 110 | elm.setAttribute('contenteditable', 'true'); 111 | if (focus) { 112 | elm.focus(); 113 | } 114 | } 115 | return elm || null; 116 | }; 117 | 118 | /** 119 | * remove contenteditable attribute from element 120 | * @param {object} [elm] - Element 121 | * @returns {object} - Element 122 | */ 123 | export const removeElementContentEditable = elm => { 124 | if (elm?.nodeType === Node.ELEMENT_NODE) { 125 | elm.removeAttribute('contenteditable'); 126 | } 127 | return elm || null; 128 | }; 129 | 130 | /** 131 | * set element dataset 132 | * @param {object} [elm] - Element 133 | * @param {string} [key] - dataset key 134 | * @param {string} [value] - dataset value 135 | * @returns {object} - Element 136 | */ 137 | export const setElementDataset = (elm, key, value) => { 138 | if (elm?.nodeType === Node.ELEMENT_NODE && 139 | isString(key) && isString(value)) { 140 | elm.dataset[key] = value; 141 | } 142 | return elm; 143 | }; 144 | -------------------------------------------------------------------------------- /src/mjs/localize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * localize.js 3 | */ 4 | 5 | /* shared */ 6 | import { DATA_I18N, EXT_LOCALE } from './constant.js'; 7 | 8 | /* api */ 9 | const { i18n } = browser; 10 | 11 | /** 12 | * localize attribute value 13 | * @param {object} [elm] - element 14 | * @returns {Promise.} - void 15 | */ 16 | export const localizeAttr = async elm => { 17 | if (elm?.nodeType === Node.ELEMENT_NODE && elm?.hasAttribute(DATA_I18N)) { 18 | const [id] = elm.getAttribute(DATA_I18N).split(/\s*,\s*/); 19 | if (id) { 20 | const attrs = { 21 | alt: 'alt', 22 | ariaLabel: 'aria-label', 23 | href: 'href', 24 | placeholder: 'placeholder', 25 | title: 'title' 26 | }; 27 | const items = Object.entries(attrs); 28 | for (const item of items) { 29 | const [key, value] = item; 30 | if (elm.hasAttribute(value)) { 31 | elm.setAttribute(value, i18n.getMessage(`${id}_${key}`)); 32 | } 33 | } 34 | } 35 | } 36 | }; 37 | 38 | /** 39 | * localize html 40 | * @returns {Promise.} - void 41 | */ 42 | export const localizeHtml = async () => { 43 | const lang = i18n.getMessage(EXT_LOCALE); 44 | if (lang) { 45 | const nodes = document.querySelectorAll(`[${DATA_I18N}]`); 46 | for (const node of nodes) { 47 | const [id, ph] = node.getAttribute(DATA_I18N).split(/\s*,\s*/); 48 | const data = i18n.getMessage(id, ph); 49 | if (data) { 50 | node.textContent = data; 51 | } 52 | localizeAttr(node); 53 | } 54 | document.documentElement.setAttribute('lang', lang); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/mjs/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * menu.js 3 | */ 4 | 5 | /* shared */ 6 | import menuItems from './menu-items.js'; 7 | import { getAllContextualIdentities } from './browser.js'; 8 | import { getType, isObjectNotEmpty, isString, throwErr } from './common.js'; 9 | import { NEW_TAB_OPEN_CONTAINER, TAB_REOPEN_CONTAINER } from './constant.js'; 10 | 11 | /* api */ 12 | const { menus, runtime } = browser; 13 | 14 | /* constant */ 15 | const ICON_SIZE_16 = 16; 16 | 17 | /* menu item map */ 18 | export const menuItemMap = new Map(); 19 | 20 | /** 21 | * update context menu 22 | * @param {string} [menuItemId] - menu item ID 23 | * @param {object} [data] - update items data 24 | * @returns {Promise.} - results of each handler 25 | */ 26 | export const updateContextMenu = async (menuItemId, data) => { 27 | const func = []; 28 | if (isString(menuItemId) && isObjectNotEmpty(data) && 29 | (Object.prototype.hasOwnProperty.call(data, 'contexts') || 30 | Object.prototype.hasOwnProperty.call(data, 'enabled') || 31 | Object.prototype.hasOwnProperty.call(data, 'icons') || 32 | Object.prototype.hasOwnProperty.call(data, 'parentId') || 33 | Object.prototype.hasOwnProperty.call(data, 'title') || 34 | Object.prototype.hasOwnProperty.call(data, 'viewTypes') || 35 | Object.prototype.hasOwnProperty.call(data, 'visible'))) { 36 | func.push(menus.update(menuItemId, data)); 37 | } 38 | return Promise.all(func); 39 | }; 40 | 41 | /** 42 | * handle create menu item callback 43 | * @returns {Promise.} - promise chain 44 | */ 45 | export const createMenuItemCallback = () => { 46 | const func = []; 47 | if (runtime.lastError) { 48 | const e = runtime.lastError; 49 | if (e.message.includes('ID already exists:')) { 50 | const [, menuItemId] = 51 | /ID\s+already\s+exists:\s+([\dA-Za-z]+)$/.exec(e.message); 52 | const data = menuItemId && menuItemMap.has(menuItemId) && 53 | menuItemMap.get(menuItemId); 54 | if (data) { 55 | func.push(updateContextMenu(menuItemId, data)); 56 | } else { 57 | throw e; 58 | } 59 | } else { 60 | throw e; 61 | } 62 | } 63 | return Promise.all(func).catch(throwErr); 64 | }; 65 | 66 | /** 67 | * create context menu item 68 | * @param {object} [data] - context data 69 | * @returns {Promise.} - menu item ID 70 | */ 71 | export const createMenuItem = async data => { 72 | let menuItemId; 73 | if (isObjectNotEmpty(data)) { 74 | const { 75 | contexts, enabled, icons, id, parentId, title, type, viewTypes, visible 76 | } = data; 77 | if (isString(id)) { 78 | await menuItemMap.set(id, { 79 | contexts, enabled, icons, parentId, title, type, viewTypes, visible 80 | }); 81 | menuItemId = await menus.create({ 82 | contexts, enabled, icons, id, parentId, title, type, viewTypes, visible 83 | }, createMenuItemCallback); 84 | } 85 | } 86 | return menuItemId || null; 87 | }; 88 | 89 | /** 90 | * create contextual identities menu 91 | * @param {object} [info] - info 92 | * @returns {Promise.} - results of each handler 93 | */ 94 | export const createContextualIdentitiesMenu = async info => { 95 | const func = []; 96 | if (isObjectNotEmpty(info)) { 97 | const { color, cookieStoreId, icon, name } = info; 98 | if (!isString(color)) { 99 | throw new TypeError(`Expected String but got ${getType(color)}.`); 100 | } 101 | if (!isString(cookieStoreId)) { 102 | throw new TypeError(`Expected String but got ${getType(cookieStoreId)}.`); 103 | } 104 | if (!isString(icon)) { 105 | throw new TypeError(`Expected String but got ${getType(icon)}.`); 106 | } 107 | if (!isString(name)) { 108 | throw new TypeError(`Expected String but got ${getType(name)}.`); 109 | } 110 | const icons = { 111 | [ICON_SIZE_16]: `img/${icon}.svg#${color}` 112 | }; 113 | const reopenOpt = { 114 | icons, 115 | contexts: ['tab'], 116 | enabled: true, 117 | id: `${cookieStoreId}Reopen`, 118 | parentId: TAB_REOPEN_CONTAINER, 119 | title: name, 120 | type: 'normal', 121 | viewTypes: ['sidebar'], 122 | visible: true 123 | }; 124 | const newTabOpt = { 125 | icons, 126 | contexts: ['page'], 127 | enabled: true, 128 | id: `${cookieStoreId}NewTab`, 129 | parentId: NEW_TAB_OPEN_CONTAINER, 130 | title: name, 131 | type: 'normal', 132 | viewTypes: ['sidebar'], 133 | visible: true 134 | }; 135 | func.push( 136 | createMenuItem(reopenOpt), 137 | createMenuItem(newTabOpt) 138 | ); 139 | } 140 | return Promise.all(func); 141 | }; 142 | 143 | /** 144 | * create context menu 145 | * @param {object} [menu] - menu 146 | * @param {string} [parentId] - parent menu item ID 147 | * @returns {Promise.} - results of each handler 148 | */ 149 | export const createContextMenu = async (menu = menuItems, parentId = null) => { 150 | const items = Object.keys(menu); 151 | const func = []; 152 | for (const item of items) { 153 | const { 154 | contexts, enabled, id, subItems, title, type, viewTypes, visible 155 | } = menu[item]; 156 | const opt = { 157 | contexts, enabled, id, parentId, title, type, viewTypes, visible 158 | }; 159 | func.push(createMenuItem(opt)); 160 | if (subItems) { 161 | func.push(createContextMenu(subItems, id)); 162 | } 163 | } 164 | if (!parentId) { 165 | const contextualIds = await getAllContextualIdentities(); 166 | if (contextualIds) { 167 | for (const item of contextualIds) { 168 | func.push(createContextualIdentitiesMenu(item)); 169 | } 170 | } 171 | } 172 | return Promise.all(func); 173 | }; 174 | 175 | /** 176 | * update contextual identities menu 177 | * @param {object} [info] - contextual identities info 178 | * @returns {Promise.} - results of each handler 179 | */ 180 | export const updateContextualIdentitiesMenu = async (info = {}) => { 181 | const { contextualIdentity } = info; 182 | const func = []; 183 | if (isObjectNotEmpty(contextualIdentity)) { 184 | const { color, cookieStoreId, icon, name } = contextualIdentity; 185 | if (!isString(color)) { 186 | throw new TypeError(`Expected String but got ${getType(color)}.`); 187 | } 188 | if (!isString(cookieStoreId)) { 189 | throw new TypeError(`Expected String but got ${getType(cookieStoreId)}.`); 190 | } 191 | if (!isString(icon)) { 192 | throw new TypeError(`Expected String but got ${getType(icon)}.`); 193 | } 194 | if (!isString(name)) { 195 | throw new TypeError(`Expected String but got ${getType(name)}.`); 196 | } 197 | const icons = { 198 | [ICON_SIZE_16]: `img/${icon}.svg#${color}` 199 | }; 200 | const reopenOpt = { 201 | icons, 202 | contexts: ['tab'], 203 | enabled: true, 204 | parentId: TAB_REOPEN_CONTAINER, 205 | title: name, 206 | type: 'normal', 207 | viewTypes: ['sidebar'], 208 | visible: true 209 | }; 210 | const newTabOpt = { 211 | icons, 212 | contexts: ['tab'], 213 | enabled: true, 214 | parentId: NEW_TAB_OPEN_CONTAINER, 215 | title: name, 216 | type: 'normal', 217 | viewTypes: ['sidebar'], 218 | visible: true 219 | }; 220 | func.push( 221 | menus.update(`${cookieStoreId}Reopen`, reopenOpt), 222 | menus.update(`${cookieStoreId}NewTab`, newTabOpt) 223 | ); 224 | } 225 | return Promise.all(func); 226 | }; 227 | 228 | /** 229 | * remove contextual identities menu 230 | * @param {object} [info] - contextual identities info 231 | * @returns {Promise.} - results of each handler 232 | */ 233 | export const removeContextualIdentitiesMenu = async info => { 234 | const func = []; 235 | if (isObjectNotEmpty(info)) { 236 | const { cookieStoreId } = info; 237 | if (isString(cookieStoreId)) { 238 | func.push( 239 | menus.remove(`${cookieStoreId}Reopen`), 240 | menus.remove(`${cookieStoreId}NewTab`) 241 | ); 242 | } 243 | } 244 | return Promise.all(func); 245 | }; 246 | 247 | /** 248 | * restore context menu 249 | * @returns {Promise} - promise chain 250 | */ 251 | export const restoreContextMenu = async () => { 252 | await menus.removeAll(); 253 | return createContextMenu(menuItems); 254 | }; 255 | 256 | /** 257 | * override context menu 258 | * @param {object} opt - options 259 | * @returns {Promise} - menus.overrideContext() 260 | */ 261 | export const overrideContextMenu = async (opt = {}) => 262 | menus.overrideContext(opt); 263 | -------------------------------------------------------------------------------- /src/mjs/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * options.js 3 | */ 4 | 5 | /* shared */ 6 | import { throwErr } from './common.js'; 7 | import { localizeHtml } from './localize.js'; 8 | import { 9 | addBookmarkLocations, addCustomThemeListener, addInitCustomThemeListener, 10 | addInitExtensionListener, addInputChangeListener, addUserCssListener, 11 | handleMsg, requestCustomTheme, setValuesFromStorage 12 | } from './options-main.js'; 13 | 14 | /* api */ 15 | const { runtime } = browser; 16 | 17 | runtime.onMessage.addListener((msg, sender) => 18 | handleMsg(msg, sender).catch(throwErr) 19 | ); 20 | 21 | /* startup */ 22 | document.addEventListener('DOMContentLoaded', () => Promise.all([ 23 | addBookmarkLocations().then(setValuesFromStorage), 24 | addCustomThemeListener(), 25 | addInitCustomThemeListener(), 26 | addInitExtensionListener(), 27 | addInputChangeListener(), 28 | addUserCssListener(), 29 | localizeHtml(), 30 | requestCustomTheme(true) 31 | ]).catch(throwErr)); 32 | -------------------------------------------------------------------------------- /src/mjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /src/mjs/port.js: -------------------------------------------------------------------------------- 1 | /** 2 | * port.js 3 | */ 4 | 5 | import { getCurrentWindow, makeConnection } from './browser.js'; 6 | import { logErr, throwErr } from './common.js'; 7 | import { SIDEBAR } from './constant.js'; 8 | 9 | /* api */ 10 | const { windows } = browser; 11 | 12 | /* constants */ 13 | const { WINDOW_ID_NONE } = windows; 14 | 15 | /* ports */ 16 | export const ports = new Map(); 17 | 18 | /** 19 | * remove port 20 | * @param {string} portId - port ID 21 | * @returns {Promise.} - result 22 | */ 23 | export const removePort = async portId => ports.delete(portId); 24 | 25 | /** 26 | * port on disconnect 27 | * @param {object} [port] - runtime.Port 28 | * @returns {Promise} - promise chain 29 | */ 30 | export const portOnDisconnect = (port = {}) => { 31 | const { error, name: portId } = port; 32 | const func = []; 33 | func.push(removePort(portId)); 34 | if (error) { 35 | func.push(logErr(error)); 36 | } 37 | return Promise.all(func).catch(throwErr); 38 | }; 39 | 40 | /** 41 | * add port 42 | * @param {string} [portId] - port ID 43 | * @returns {Promise.} - runtime.Port 44 | */ 45 | export const addPort = async portId => { 46 | const { id: windowId } = await getCurrentWindow(); 47 | let port; 48 | if (windowId !== WINDOW_ID_NONE) { 49 | portId ??= `${SIDEBAR}_${windowId}`; 50 | if (ports.has(portId)) { 51 | port = ports.get(portId); 52 | } else { 53 | port = await makeConnection({ 54 | name: portId 55 | }); 56 | port.onDisconnect.addListener(portOnDisconnect); 57 | ports.set(portId, port); 58 | } 59 | } 60 | return port || null; 61 | }; 62 | 63 | /** 64 | * get port 65 | * @param {string} [portId] - port ID 66 | * @param {boolean} [add] - add port if port does not exist 67 | * @returns {Promise.} - runtime.Port 68 | */ 69 | export const getPort = async (portId, add = false) => { 70 | let port; 71 | if (portId && ports.has(portId)) { 72 | port = ports.get(portId); 73 | } else if (add) { 74 | port = await addPort(portId); 75 | } 76 | return port || null; 77 | }; 78 | -------------------------------------------------------------------------------- /src/mjs/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * session.js 3 | */ 4 | 5 | /* shared */ 6 | import { 7 | getCurrentWindow, getSessionWindowValue, getWindow, setSessionWindowValue 8 | } from './browser.js'; 9 | import { getType, isObjectNotEmpty, isString } from './common.js'; 10 | import { getPort } from './port.js'; 11 | import { 12 | CLASS_HEADING, CLASS_HEADING_LABEL, CLASS_TAB_COLLAPSED, CLASS_TAB_CONTAINER, 13 | NEW_TAB, SESSION_SAVE, SIDEBAR, TAB_LIST, TAB_QUERY 14 | } from './constant.js'; 15 | 16 | /** 17 | * get tab list from sessions 18 | * @param {string} key - key 19 | * @param {number} [windowId] - window ID 20 | * @returns {Promise.} - tab list 21 | */ 22 | export const getSessionTabList = async (key, windowId) => { 23 | if (!isString(key)) { 24 | throw new TypeError(`Expected String but got ${getType(key)}.`); 25 | } 26 | if (!Number.isInteger(windowId)) { 27 | const win = await getCurrentWindow(); 28 | windowId = win.id; 29 | } 30 | const value = await getSessionWindowValue(key, windowId); 31 | let tabList; 32 | if (isString(value)) { 33 | tabList = JSON.parse(value); 34 | } 35 | return tabList || null; 36 | }; 37 | 38 | /* mutex */ 39 | export const mutex = new Set(); 40 | 41 | /** 42 | * save tab list to sessions 43 | * @param {string} domStr - DOM string 44 | * @param {number} windowId - window ID 45 | * @returns {Promise.} - saved 46 | */ 47 | export const saveSessionTabList = async (domStr, windowId) => { 48 | if (!isString(domStr)) { 49 | throw new TypeError(`Expected String but got ${getType(domStr)}.`); 50 | } 51 | if (!Number.isInteger(windowId)) { 52 | throw new TypeError(`Expected Number but got ${getType(windowId)}.`); 53 | } 54 | const win = await getWindow(windowId); 55 | const { incognito } = win; 56 | let res; 57 | if (!incognito && !mutex.has(windowId)) { 58 | mutex.add(windowId); 59 | try { 60 | const tabList = { 61 | recent: {} 62 | }; 63 | const dom = new DOMParser().parseFromString(domStr, 'text/html'); 64 | const items = 65 | dom.querySelectorAll(`.${CLASS_TAB_CONTAINER}:not(#${NEW_TAB})`); 66 | const prevList = await getSessionTabList(TAB_LIST, windowId); 67 | const l = items.length; 68 | let i = 0; 69 | let j = 0; 70 | while (i < l) { 71 | const item = items[i]; 72 | const collapsed = item.classList.contains(CLASS_TAB_COLLAPSED); 73 | const heading = item.querySelector(`.${CLASS_HEADING}`); 74 | const headingShown = heading && !heading.hidden; 75 | const headingLabel = 76 | heading && 77 | heading.querySelector(`.${CLASS_HEADING_LABEL}`).textContent; 78 | const childTabs = item.querySelectorAll(TAB_QUERY); 79 | for (const tab of childTabs) { 80 | const tabsTab = tab.dataset.tab; 81 | const { url } = JSON.parse(tabsTab); 82 | tabList.recent[j] = { 83 | collapsed, 84 | headingLabel, 85 | headingShown, 86 | url, 87 | containerIndex: i 88 | }; 89 | j++; 90 | } 91 | i++; 92 | } 93 | if (isObjectNotEmpty(prevList) && 94 | Object.prototype.hasOwnProperty.call(prevList, 'recent')) { 95 | tabList.prev = Object.assign({}, prevList.recent); 96 | } 97 | await setSessionWindowValue(TAB_LIST, JSON.stringify(tabList), windowId); 98 | res = mutex.delete(windowId); 99 | } catch (e) { 100 | mutex.delete(windowId); 101 | throw e; 102 | } 103 | } 104 | return !!res; 105 | }; 106 | 107 | /** 108 | * request save session 109 | * @returns {Promise.} - port.postMessage() 110 | */ 111 | export const requestSaveSession = async () => { 112 | const { id: windowId, incognito } = await getCurrentWindow(); 113 | const port = await getPort(`${SIDEBAR}_${windowId}`, true); 114 | let func; 115 | if (port && !incognito) { 116 | const clonedBody = document.body.cloneNode(true); 117 | const items = 118 | clonedBody.querySelectorAll(`.${CLASS_TAB_CONTAINER}:not(#${NEW_TAB})`); 119 | const frag = document.createDocumentFragment(); 120 | frag.append(...items); 121 | if (frag.childElementCount) { 122 | const doctype = new XMLSerializer().serializeToString(document.doctype); 123 | const dom = new XMLSerializer().serializeToString(frag); 124 | func = port.postMessage({ 125 | [SESSION_SAVE]: { 126 | windowId, 127 | domString: `${doctype}${dom}` 128 | } 129 | }); 130 | } 131 | } 132 | return func || null; 133 | }; 134 | 135 | // For test 136 | export { ports } from './port.js'; 137 | -------------------------------------------------------------------------------- /src/mjs/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sidebar.js 3 | */ 4 | 5 | /* shared */ 6 | import { throwErr } from './common.js'; 7 | import { 8 | getLastClosedTab, handleActivatedTab, handleAttachedTab, handleClickedMenu, 9 | handleContextmenuEvt, handleCreatedTab, handleDetachedTab, handleEvt, 10 | handleHighlightedTab, handleMovedTab, handleMsg, handleRemovedTab, 11 | handleStorage, handleUpdatedTab, handleUpdatedTheme, restoreHighlightedTabs, 12 | setContextualIds, startup 13 | } from './main.js'; 14 | import { requestSaveSession } from './session.js'; 15 | import { 16 | expandActivatedCollapsedTab, restoreTabContainers 17 | } from './tab-group.js'; 18 | import { COLOR_SCHEME_DARK } from './constant.js'; 19 | 20 | /* api */ 21 | const { contextualIdentities, menus, runtime, storage, tabs, theme } = browser; 22 | 23 | /* listeners */ 24 | contextualIdentities.onCreated.addListener(() => 25 | setContextualIds().catch(throwErr) 26 | ); 27 | contextualIdentities.onRemoved.addListener(() => 28 | setContextualIds().catch(throwErr) 29 | ); 30 | contextualIdentities.onUpdated.addListener(() => 31 | setContextualIds().catch(throwErr) 32 | ); 33 | menus.onClicked.addListener(info => handleClickedMenu(info).catch(throwErr)); 34 | runtime.onMessage.addListener((msg, sender) => 35 | handleMsg(msg, sender).catch(throwErr) 36 | ); 37 | storage.onChanged.addListener((data, area) => 38 | handleStorage(data, area, true).catch(throwErr) 39 | ); 40 | tabs.onActivated.addListener(info => 41 | handleActivatedTab(info).then(expandActivatedCollapsedTab) 42 | .then(requestSaveSession).catch(throwErr) 43 | ); 44 | tabs.onAttached.addListener((tabId, info) => 45 | handleAttachedTab(tabId, info).then(restoreTabContainers) 46 | .then(restoreHighlightedTabs).then(requestSaveSession).catch(throwErr) 47 | ); 48 | tabs.onCreated.addListener(tabsTab => 49 | handleCreatedTab(tabsTab).then(restoreTabContainers) 50 | .then(requestSaveSession).then(getLastClosedTab).catch(throwErr) 51 | ); 52 | tabs.onDetached.addListener((tabId, info) => 53 | handleDetachedTab(tabId, info).then(restoreTabContainers) 54 | .then(expandActivatedCollapsedTab).then(requestSaveSession) 55 | .catch(throwErr) 56 | ); 57 | tabs.onHighlighted.addListener(info => 58 | handleHighlightedTab(info).catch(throwErr) 59 | ); 60 | tabs.onMoved.addListener((tabId, info) => 61 | handleMovedTab(tabId, info).catch(throwErr) 62 | ); 63 | tabs.onRemoved.addListener((tabId, info) => 64 | handleRemovedTab(tabId, info).then(restoreTabContainers) 65 | .then(expandActivatedCollapsedTab).then(requestSaveSession) 66 | .then(getLastClosedTab).catch(throwErr) 67 | ); 68 | tabs.onUpdated.addListener( 69 | (tabId, info, tabsTab) => 70 | handleUpdatedTab(tabId, info, tabsTab).catch(throwErr), 71 | { 72 | properties: [ 73 | 'audible', 'discarded', 'favIconUrl', 'hidden', 'mutedInfo', 'pinned', 74 | 'status', 'title', 'url' 75 | ] 76 | } 77 | ); 78 | theme.onUpdated.addListener(info => handleUpdatedTheme(info).catch(throwErr)); 79 | 80 | window.addEventListener('keydown', handleEvt, true); 81 | window.addEventListener('mousedown', handleEvt, true); 82 | window.addEventListener('contextmenu', handleContextmenuEvt); 83 | window.matchMedia(COLOR_SCHEME_DARK).addEventListener('change', () => 84 | handleUpdatedTheme().catch(throwErr) 85 | ); 86 | document.addEventListener('DOMContentLoaded', () => startup().catch(throwErr)); 87 | -------------------------------------------------------------------------------- /src/web-ext-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignoreFiles: [ 3 | './mjs/package.json', 4 | 'web-ext-config.cjs' 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/common.test.js: -------------------------------------------------------------------------------- 1 | /* api */ 2 | import { strict as assert } from 'node:assert'; 3 | import { describe, it } from 'mocha'; 4 | import sinon from 'sinon'; 5 | 6 | /* test */ 7 | import { 8 | getType, isString, logErr, logMsg, logWarn, throwErr 9 | } from '../scripts/common.js'; 10 | 11 | describe('getType', () => { 12 | it('should get Undefined', () => { 13 | assert.strictEqual(getType(), 'Undefined'); 14 | }); 15 | 16 | it('should get Null', () => { 17 | assert.strictEqual(getType(null), 'Null'); 18 | }); 19 | 20 | it('should get Object', () => { 21 | assert.strictEqual(getType({}), 'Object'); 22 | }); 23 | 24 | it('should get Array', () => { 25 | assert.strictEqual(getType([]), 'Array'); 26 | }); 27 | 28 | it('should get Boolean', () => { 29 | assert.strictEqual(getType(true), 'Boolean'); 30 | }); 31 | 32 | it('should get Number', () => { 33 | assert.strictEqual(getType(1), 'Number'); 34 | }); 35 | 36 | it('should get String', () => { 37 | assert.strictEqual(getType('a'), 'String'); 38 | }); 39 | }); 40 | 41 | describe('isString', () => { 42 | it('should get true if string is given', () => { 43 | assert.strictEqual(isString('a'), true); 44 | }); 45 | 46 | it('should get false if given argument is not string', () => { 47 | assert.strictEqual(isString(1), false); 48 | }); 49 | }); 50 | 51 | describe('logErr', () => { 52 | it('should get false', () => { 53 | const msg = 'Log Error test'; 54 | let errMsg; 55 | const consoleError = sinon.stub(console, 'error').callsFake(e => { 56 | errMsg = e.message; 57 | }); 58 | const res = logErr(new Error(msg)); 59 | const { calledOnce } = consoleError; 60 | consoleError.restore(); 61 | assert.strictEqual(calledOnce, true); 62 | assert.strictEqual(errMsg, msg); 63 | assert.strictEqual(res, false); 64 | }); 65 | }); 66 | 67 | describe('logMsg', () => { 68 | it('should get string', () => { 69 | const msg = 'Log message test'; 70 | let logMessage; 71 | const consoleLog = sinon.stub(console, 'log').callsFake(m => { 72 | logMessage = m; 73 | }); 74 | const res = logMsg(msg); 75 | const { calledOnce } = consoleLog; 76 | consoleLog.restore(); 77 | assert.strictEqual(calledOnce, true); 78 | assert.strictEqual(logMessage, msg); 79 | assert.strictEqual(res, msg); 80 | }); 81 | }); 82 | 83 | describe('logWarn', () => { 84 | it('should get false', () => { 85 | const msg = 'Log warn test'; 86 | let warnMsg; 87 | const consoleWarn = sinon.stub(console, 'warn').callsFake(m => { 88 | warnMsg = m; 89 | }); 90 | const res = logWarn(msg); 91 | const { calledOnce } = consoleWarn; 92 | consoleWarn.restore(); 93 | assert.strictEqual(calledOnce, true); 94 | assert.strictEqual(warnMsg, msg); 95 | assert.strictEqual(res, false); 96 | }); 97 | }); 98 | 99 | describe('throwErr', () => { 100 | it('should throw', () => { 101 | const e = new Error('Error'); 102 | assert.throws(() => throwErr(e)); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/constant.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * constant.test.js 3 | */ 4 | 5 | /* api */ 6 | import { strict as assert } from 'node:assert'; 7 | import { describe, it } from 'mocha'; 8 | 9 | /* test */ 10 | import * as mjs from '../src/mjs/constant.js'; 11 | 12 | describe('constants', () => { 13 | const items = Object.entries(mjs); 14 | for (const [key, value] of items) { 15 | it('should get string', () => { 16 | assert.strictEqual(typeof key, 'string'); 17 | assert.strictEqual(typeof value, 'string'); 18 | }); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /test/file-util.test.js: -------------------------------------------------------------------------------- 1 | /* api */ 2 | import { strict as assert } from 'node:assert'; 3 | import fs, { promises as fsPromise } from 'node:fs'; 4 | import os from 'node:os'; 5 | import path from 'node:path'; 6 | import { afterEach, beforeEach, describe, it } from 'mocha'; 7 | import { MockAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'; 8 | 9 | /* test */ 10 | import { 11 | createFile, fetchText, getStat, isDir, isFile, readFile, removeDir 12 | } from '../scripts/file-util.js'; 13 | 14 | /* constants */ 15 | const TMPDIR = process.env.TMP || process.env.TMPDIR || process.env.TEMP || 16 | os.tmpdir(); 17 | 18 | describe('getStat', () => { 19 | it('should be an object', () => { 20 | const p = path.resolve('test', 'file', 'test.txt'); 21 | assert.strictEqual(typeof getStat(p), 'object', 'mode'); 22 | assert.notDeepEqual(getStat(p), null, 'mode'); 23 | }); 24 | 25 | it('should get null if given argument is not string', () => { 26 | assert.strictEqual(getStat(), null); 27 | }); 28 | 29 | it('should get null if file does not exist', () => { 30 | const p = path.resolve('test', 'file', 'foo.txt'); 31 | assert.strictEqual(getStat(p), null); 32 | }); 33 | }); 34 | 35 | describe('isDir', () => { 36 | it('should get true if dir exists', () => { 37 | const p = path.resolve(path.join('test', 'file')); 38 | assert.strictEqual(isDir(p), true); 39 | }); 40 | 41 | it('should get false if dir does not exist', () => { 42 | const p = path.resolve(path.join('test', 'foo')); 43 | assert.strictEqual(isDir(p), false); 44 | }); 45 | }); 46 | 47 | describe('isFile', () => { 48 | it('should get true if file exists', () => { 49 | const p = path.resolve('test', 'file', 'test.txt'); 50 | assert.strictEqual(isFile(p), true); 51 | }); 52 | 53 | it('should get false if file does not exist', () => { 54 | const p = path.resolve('test', 'file', 'foo.txt'); 55 | assert.strictEqual(isFile(p), false); 56 | }); 57 | }); 58 | 59 | describe('removeDir', () => { 60 | it('should throw', () => { 61 | const foo = path.resolve('foo'); 62 | assert.strictEqual(isDir(foo), false); 63 | assert.throws(() => removeDir(foo), Error, `No such directory: ${foo}`); 64 | }); 65 | 66 | it("should remove dir and it's files", async () => { 67 | const dirPath = path.join(TMPDIR, 'url-sanitizer'); 68 | fs.mkdirSync(dirPath); 69 | const subDirPath = path.join(dirPath, 'foo'); 70 | fs.mkdirSync(subDirPath); 71 | const filePath = path.join(subDirPath, 'test.txt'); 72 | const value = 'test file.\n'; 73 | await fsPromise.writeFile(filePath, value, { 74 | encoding: 'utf8', flag: 'w', mode: 0o666 75 | }); 76 | const res1 = await Promise.all([ 77 | fs.existsSync(dirPath), 78 | fs.existsSync(subDirPath), 79 | fs.existsSync(filePath) 80 | ]); 81 | removeDir(dirPath); 82 | const res2 = await Promise.all([ 83 | fs.existsSync(dirPath), 84 | fs.existsSync(subDirPath), 85 | fs.existsSync(filePath) 86 | ]); 87 | assert.deepEqual(res1, [true, true, true]); 88 | assert.deepEqual(res2, [false, false, false]); 89 | }); 90 | }); 91 | 92 | describe('readFile', () => { 93 | it('should throw', async () => { 94 | await readFile('foo/bar').catch(e => { 95 | assert.strictEqual(e.message, 'foo/bar is not a file.'); 96 | }); 97 | }); 98 | 99 | it('should get file', async () => { 100 | const p = path.resolve('test', 'file', 'test.txt'); 101 | const opt = { encoding: 'utf8', flag: 'r' }; 102 | const file = await readFile(p, opt); 103 | assert.strictEqual(file, 'test file\n'); 104 | }); 105 | }); 106 | 107 | describe('createFile', () => { 108 | const dirPath = path.join(TMPDIR, 'sidebartabs'); 109 | beforeEach(() => { 110 | fs.rmSync(dirPath, { force: true, recursive: true }); 111 | }); 112 | afterEach(() => { 113 | fs.rmSync(dirPath, { force: true, recursive: true }); 114 | }); 115 | 116 | it('should get string', async () => { 117 | fs.mkdirSync(dirPath); 118 | const filePath = path.join(dirPath, 'test.txt'); 119 | const value = 'test file.\n'; 120 | const file = await createFile(filePath, value); 121 | assert.strictEqual(file, filePath); 122 | }); 123 | 124 | it('should throw if first argument is not a string', () => { 125 | createFile().catch(e => { 126 | assert.strictEqual(e.message, 'Expected String but got Undefined.'); 127 | }); 128 | }); 129 | 130 | it('should throw if second argument is not a string', () => { 131 | const file = path.join(dirPath, 'test.txt'); 132 | createFile(file).catch(e => { 133 | assert.strictEqual(e.message, 'Expected String but got Undefined.'); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('fetch text', () => { 139 | const globalDispatcher = getGlobalDispatcher(); 140 | const mockAgent = new MockAgent(); 141 | beforeEach(() => { 142 | setGlobalDispatcher(mockAgent); 143 | mockAgent.disableNetConnect(); 144 | }); 145 | afterEach(() => { 146 | mockAgent.enableNetConnect(); 147 | setGlobalDispatcher(globalDispatcher); 148 | }); 149 | 150 | it('should throw', async () => { 151 | await fetchText().catch(e => { 152 | assert.strictEqual(e instanceof TypeError, true, 'error'); 153 | assert.strictEqual(e.message, 'Expected String but got Undefined.'); 154 | }); 155 | }); 156 | 157 | it('should throw', async () => { 158 | const base = 'https://example.com'; 159 | mockAgent.get(base).intercept({ path: '/', method: 'GET' }).reply(404); 160 | await fetchText(base).catch(e => { 161 | assert.strictEqual(e instanceof Error, true, 'error'); 162 | assert.strictEqual(e.message, 163 | `Network response was not ok. status: 404 url: ${base}`); 164 | }); 165 | }); 166 | 167 | it('should get result', async () => { 168 | const base = 'https://example.com'; 169 | mockAgent.get(base).intercept({ path: '/', method: 'GET' }) 170 | .reply(200, 'foo'); 171 | const res = await fetchText('https://example.com'); 172 | assert.strictEqual(res, 'foo', 'result'); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/file/test.txt: -------------------------------------------------------------------------------- 1 | test file 2 | -------------------------------------------------------------------------------- /test/localize.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * localize.test.js 3 | */ 4 | /* eslint-disable import-x/order */ 5 | 6 | /* api */ 7 | import { strict as assert } from 'node:assert'; 8 | import { afterEach, beforeEach, describe, it } from 'mocha'; 9 | import { browser, createJsdom } from './mocha/setup.js'; 10 | 11 | /* test */ 12 | import * as mjs from '../src/mjs/localize.js'; 13 | import { EXT_LOCALE } from '../src/mjs/constant.js'; 14 | 15 | describe('localize', () => { 16 | let window, document; 17 | beforeEach(() => { 18 | const dom = createJsdom(); 19 | window = dom.window; 20 | document = dom.window.document; 21 | browser._sandbox.reset(); 22 | browser.i18n.getMessage.callsFake((...args) => args.toString()); 23 | browser.permissions.contains.resolves(true); 24 | global.browser = browser; 25 | global.window = window; 26 | global.document = document; 27 | }); 28 | afterEach(() => { 29 | window = null; 30 | document = null; 31 | delete global.browser; 32 | delete global.window; 33 | delete global.document; 34 | browser._sandbox.reset(); 35 | }); 36 | 37 | describe('localize attribute value', () => { 38 | const func = mjs.localizeAttr; 39 | const globalKeys = ['Node', 'NodeList']; 40 | beforeEach(() => { 41 | for (const key of globalKeys) { 42 | global[key] = window[key]; 43 | } 44 | }); 45 | afterEach(() => { 46 | for (const key of globalKeys) { 47 | delete global[key]; 48 | } 49 | }); 50 | 51 | it('should not call function if no argument given', async () => { 52 | const i = browser.i18n.getMessage.callCount; 53 | await func(); 54 | assert.strictEqual(browser.i18n.getMessage.callCount, i, 'not called'); 55 | }); 56 | 57 | it('should not call function if argument is not an element', async () => { 58 | const i = browser.i18n.getMessage.callCount; 59 | await func('foo'); 60 | assert.strictEqual(browser.i18n.getMessage.callCount, i, 'not called'); 61 | }); 62 | 63 | it('should set attribute', async () => { 64 | const id = 'foo'; 65 | const attrs = { 66 | alt: 'alt', 67 | ariaLabel: 'aria-label', 68 | href: 'href', 69 | placeholder: 'placeholder', 70 | title: 'title' 71 | }; 72 | const items = Object.entries(attrs); 73 | const p = document.createElement('p'); 74 | const body = document.querySelector('body'); 75 | for (const [key, value] of items) { 76 | p.setAttribute(value, 'bar'); 77 | browser.i18n.getMessage.withArgs(`${id}_${key}`) 78 | .returns(`${id}_${key}`); 79 | } 80 | p.setAttribute('data-i18n', id); 81 | body.appendChild(p); 82 | await func(p); 83 | for (const [key, value] of items) { 84 | assert.strictEqual(p.getAttribute(value), `${id}_${key}`, `${value}`); 85 | } 86 | }); 87 | 88 | it('should not set attribute', async () => { 89 | const id = ''; 90 | const attrs = { 91 | alt: 'alt', 92 | ariaLabel: 'aria-label', 93 | href: 'href', 94 | placeholder: 'placeholder', 95 | title: 'title' 96 | }; 97 | const items = Object.values(attrs); 98 | const p = document.createElement('p'); 99 | const body = document.querySelector('body'); 100 | for (const value of items) { 101 | p.setAttribute(value, 'bar'); 102 | } 103 | p.setAttribute('data-i18n', id); 104 | body.appendChild(p); 105 | await func(p); 106 | for (const value of items) { 107 | assert.strictEqual(p.getAttribute(value), 'bar', `${value}`); 108 | } 109 | }); 110 | }); 111 | 112 | describe('localize html', () => { 113 | const func = mjs.localizeHtml; 114 | const globalKeys = ['Node', 'NodeList']; 115 | beforeEach(() => { 116 | for (const key of globalKeys) { 117 | global[key] = window[key]; 118 | } 119 | }); 120 | afterEach(() => { 121 | for (const key of globalKeys) { 122 | delete global[key]; 123 | } 124 | }); 125 | 126 | it('should not set value', async () => { 127 | browser.i18n.getMessage.withArgs(EXT_LOCALE).returns(''); 128 | await func(); 129 | const root = document.documentElement; 130 | assert.strictEqual(root.getAttribute('lang'), null, 'lang'); 131 | }); 132 | 133 | it('should set value', async () => { 134 | browser.i18n.getMessage.withArgs(EXT_LOCALE).returns('en'); 135 | await func(); 136 | const root = document.documentElement; 137 | assert.strictEqual(root.lang, 'en', 'lang'); 138 | }); 139 | 140 | it('should set value', async () => { 141 | browser.i18n.getMessage.withArgs(EXT_LOCALE).returns('en'); 142 | browser.i18n.getMessage.withArgs('foo', 'bar').returns('baz'); 143 | const body = document.querySelector('body'); 144 | const p = document.createElement('p'); 145 | p.setAttribute('data-i18n', 'foo,bar'); 146 | body.appendChild(p); 147 | await func(); 148 | const root = document.documentElement; 149 | assert.strictEqual(root.lang, 'en', 'lang'); 150 | assert.strictEqual(p.textContent, 'baz', 'content'); 151 | }); 152 | 153 | it('should set value', async () => { 154 | browser.i18n.getMessage.withArgs(EXT_LOCALE).returns('en'); 155 | browser.i18n.getMessage.withArgs('foo', 'bar').returns(''); 156 | const body = document.querySelector('body'); 157 | const p = document.createElement('p'); 158 | p.setAttribute('data-i18n', 'foo,bar'); 159 | p.textContent = 'baz'; 160 | body.appendChild(p); 161 | await func(); 162 | const root = document.documentElement; 163 | assert.strictEqual(root.lang, 'en', 'lang'); 164 | assert.strictEqual(p.textContent, 'baz', 'content'); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/menu-items.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * menu-items.test.js 3 | */ 4 | /* eslint-disable import-x/order */ 5 | 6 | /* api */ 7 | import { strict as assert } from 'node:assert'; 8 | import { afterEach, beforeEach, describe, it } from 'mocha'; 9 | import { browser } from './mocha/setup.js'; 10 | 11 | /* test */ 12 | import menuItems from '../src/mjs/menu-items.js'; 13 | import { 14 | NEW_TAB_OPEN_CONTAINER, OPTIONS_OPEN, 15 | TAB_ALL_BOOKMARK, TAB_ALL_RELOAD, TAB_ALL_SELECT, 16 | TAB_BOOKMARK, TAB_CLOSE, TAB_CLOSE_DUPE, TAB_CLOSE_UNDO, TAB_DUPE, 17 | TAB_GROUP, TAB_GROUP_BOOKMARK, TAB_GROUP_CLOSE, TAB_GROUP_COLLAPSE, 18 | TAB_GROUP_COLLAPSE_OTHER, TAB_GROUP_CONTAINER, TAB_GROUP_DETACH, 19 | TAB_GROUP_DETACH_TABS, TAB_GROUP_DOMAIN, TAB_GROUP_LABEL_SHOW, 20 | TAB_GROUP_SELECT, TAB_GROUP_SELECTED, TAB_GROUP_UNGROUP, 21 | TAB_MOVE, TAB_MOVE_END, TAB_MOVE_START, TAB_MOVE_WIN, TAB_MUTE, TAB_NEW, 22 | TAB_PIN, TAB_RELOAD, TAB_REOPEN_CONTAINER, 23 | TABS_BOOKMARK, TABS_CLOSE, TABS_CLOSE_DUPE, TABS_CLOSE_END, TABS_CLOSE_OTHER, 24 | TABS_CLOSE_START, TABS_CLOSE_MULTIPLE, TABS_DUPE, TABS_MOVE, TABS_MOVE_END, 25 | TABS_MOVE_START, TABS_MOVE_WIN, TABS_MUTE, TABS_PIN, TABS_RELOAD, 26 | TABS_REOPEN_CONTAINER 27 | } from '../src/mjs/constant.js'; 28 | 29 | describe('menu items', () => { 30 | beforeEach(() => { 31 | browser._sandbox.reset(); 32 | browser.i18n.getMessage.callsFake((...args) => args.toString()); 33 | browser.permissions.contains.resolves(true); 34 | global.browser = browser; 35 | }); 36 | afterEach(() => { 37 | delete global.browser; 38 | browser._sandbox.reset(); 39 | }); 40 | 41 | describe('should get string and object', () => { 42 | const itemKeys = [ 43 | OPTIONS_OPEN, 44 | NEW_TAB_OPEN_CONTAINER, 'sep-0', 45 | TAB_NEW, 'sep-1', 46 | TAB_RELOAD, TABS_RELOAD, TAB_MUTE, TABS_MUTE, 'sep-2', 47 | TAB_PIN, TABS_PIN, TAB_BOOKMARK, TABS_BOOKMARK, TAB_DUPE, TABS_DUPE, 48 | TAB_REOPEN_CONTAINER, TABS_REOPEN_CONTAINER, TAB_MOVE, TABS_MOVE, 49 | TAB_ALL_RELOAD, TAB_ALL_SELECT, TAB_ALL_BOOKMARK, 'sep-3', 50 | TAB_GROUP, 'sep-4', 51 | TAB_CLOSE, TABS_CLOSE, TAB_CLOSE_DUPE, TABS_CLOSE_DUPE, 52 | TABS_CLOSE_MULTIPLE, TAB_CLOSE_UNDO 53 | ]; 54 | const items = Object.entries(menuItems); 55 | 56 | it('should get equal length', () => { 57 | assert.strictEqual(items.length, itemKeys.length, 'length'); 58 | }); 59 | 60 | it('should get string and object', () => { 61 | for (const [key, value] of items) { 62 | assert.strictEqual(itemKeys.includes(key), true, `includes ${key}`); 63 | assert.strictEqual(typeof key, 'string', 'key'); 64 | assert.strictEqual(typeof value, 'object', 'value'); 65 | } 66 | }); 67 | }); 68 | 69 | describe('sub items', () => { 70 | const parentItemKeys = [ 71 | TAB_MOVE, TABS_MOVE, 72 | TAB_GROUP, 73 | TABS_CLOSE_MULTIPLE 74 | ]; 75 | const subItemKeys = [ 76 | TAB_MOVE_START, TAB_MOVE_END, TAB_MOVE_WIN, 77 | TABS_MOVE_START, TABS_MOVE_END, TABS_MOVE_WIN, 78 | TAB_GROUP_COLLAPSE, TAB_GROUP_COLLAPSE_OTHER, 'sepTabGroup-1', 79 | TAB_GROUP_LABEL_SHOW, 'sepTabGroup-2', 80 | TAB_GROUP_BOOKMARK, TAB_GROUP_SELECT, TAB_GROUP_SELECTED, 81 | TAB_GROUP_CONTAINER, TAB_GROUP_DOMAIN, TAB_GROUP_DETACH, 82 | TAB_GROUP_DETACH_TABS, TAB_GROUP_UNGROUP, 'sepTabGroup-3', 83 | TAB_GROUP_CLOSE, 84 | TABS_CLOSE_START, TABS_CLOSE_END, TABS_CLOSE_OTHER 85 | ]; 86 | 87 | it('should get string and object of sub items', () => { 88 | parentItemKeys.forEach(itemKey => { 89 | const items = Object.entries(menuItems[itemKey].subItems); 90 | for (const [key, value] of items) { 91 | assert.strictEqual(subItemKeys.includes(key), true, 'item'); 92 | assert.strictEqual(typeof key, 'string', 'key'); 93 | assert.strictEqual(typeof value, 'object', 'value'); 94 | } 95 | }); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/mocha/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * setup.js 3 | */ 4 | 5 | import process from 'node:process'; 6 | import { DOMSelector } from '@asamuzakjp/dom-selector'; 7 | import domPurify from 'dompurify'; 8 | import { JSDOM } from 'jsdom'; 9 | import sinon from 'sinon'; 10 | import { Schema } from 'webext-schema'; 11 | 12 | /** 13 | * create jsdom 14 | * @returns {object} - jsdom instance 15 | */ 16 | export const createJsdom = () => { 17 | const domstr = ''; 18 | const opt = { 19 | runScripts: 'dangerously', 20 | beforeParse: window => { 21 | const domSelector = new DOMSelector(window); 22 | const closest = domSelector.closest.bind(domSelector); 23 | const matches = domSelector.matches.bind(domSelector); 24 | const querySelector = domSelector.querySelector.bind(domSelector); 25 | const querySelectorAll = domSelector.querySelectorAll.bind(domSelector); 26 | window.alert = sinon.stub(); 27 | window.matchMedia = sinon.stub().returns({ 28 | matches: false 29 | }); 30 | window.prompt = sinon.stub(); 31 | window.DOMPurify = domPurify; 32 | window.Element.prototype.matches = function (...args) { 33 | if (!args.length) { 34 | const msg = '1 argument required, but only 0 present.'; 35 | throw new window.TypeError(msg); 36 | } 37 | const [selector] = args; 38 | return matches(selector, this); 39 | }; 40 | window.Element.prototype.closest = function (...args) { 41 | if (!args.length) { 42 | const msg = '1 argument required, but only 0 present.'; 43 | throw new window.TypeError(msg); 44 | } 45 | const [selector] = args; 46 | return closest(selector, this); 47 | }; 48 | window.Document.prototype.querySelector = function (...args) { 49 | if (!args.length) { 50 | const msg = '1 argument required, but only 0 present.'; 51 | throw new window.TypeError(msg); 52 | } 53 | const [selector] = args; 54 | return querySelector(selector, this); 55 | }; 56 | window.DocumentFragment.prototype.querySelector = function (...args) { 57 | if (!args.length) { 58 | const msg = '1 argument required, but only 0 present.'; 59 | throw new window.TypeError(msg); 60 | } 61 | const [selector] = args; 62 | return querySelector(selector, this); 63 | }; 64 | window.Element.prototype.querySelector = function (...args) { 65 | if (!args.length) { 66 | const msg = '1 argument required, but only 0 present.'; 67 | throw new window.TypeError(msg); 68 | } 69 | const [selector] = args; 70 | return querySelector(selector, this); 71 | }; 72 | window.Document.prototype.querySelectorAll = function (...args) { 73 | if (!args.length) { 74 | const msg = '1 argument required, but only 0 present.'; 75 | throw new window.TypeError(msg); 76 | } 77 | const [selector] = args; 78 | return querySelectorAll(selector, this); 79 | }; 80 | window.DocumentFragment.prototype.querySelectorAll = function (...args) { 81 | if (!args.length) { 82 | const msg = '1 argument required, but only 0 present.'; 83 | throw new window.TypeError(msg); 84 | } 85 | const [selector] = args; 86 | return querySelectorAll(selector, this); 87 | }; 88 | window.Element.prototype.querySelectorAll = function (...args) { 89 | if (!args.length) { 90 | const msg = '1 argument required, but only 0 present.'; 91 | throw new window.TypeError(msg); 92 | } 93 | const [selector] = args; 94 | return querySelectorAll(selector, this); 95 | }; 96 | } 97 | }; 98 | return new JSDOM(domstr, opt); 99 | }; 100 | 101 | const { window } = createJsdom(); 102 | const { document } = window; 103 | 104 | /** 105 | * get channel 106 | * @returns {string} - channel 107 | */ 108 | const getChannel = () => { 109 | let ch; 110 | const reg = /(?<=--channel=)[a-z]+/; 111 | const args = process.argv.filter(arg => reg.test(arg)); 112 | if (args.length) { 113 | [ch] = reg.exec(args); 114 | } else { 115 | ch = 'beta'; 116 | } 117 | return ch; 118 | }; 119 | 120 | const channel = getChannel(); 121 | 122 | console.log(`Channel: ${channel}`); 123 | 124 | export const browser = new Schema(channel).mock(); 125 | 126 | export const mockPort = ({ name, sender }) => { 127 | const port = Object.assign({}, browser.runtime.Port); 128 | port.name = name; 129 | port.sender = sender; 130 | return port; 131 | }; 132 | 133 | browser.i18n.getMessage.callsFake((...args) => args.toString()); 134 | browser.permissions.contains.resolves(true); 135 | 136 | global.window = window; 137 | global.document = document; 138 | global.browser = browser; 139 | -------------------------------------------------------------------------------- /test/port.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * port.test.js 3 | */ 4 | /* eslint-disable import-x/order */ 5 | 6 | /* api */ 7 | import { strict as assert } from 'node:assert'; 8 | import { afterEach, beforeEach, describe, it } from 'mocha'; 9 | import sinon from 'sinon'; 10 | import { browser, mockPort } from './mocha/setup.js'; 11 | 12 | /* test */ 13 | import * as mjs from '../src/mjs/port.js'; 14 | import { SIDEBAR } from '../src/mjs/constant.js'; 15 | 16 | describe('port', () => { 17 | beforeEach(() => { 18 | browser._sandbox.reset(); 19 | browser.permissions.contains.resolves(true); 20 | browser.runtime.connect.callsFake(mockPort); 21 | global.browser = browser; 22 | }); 23 | afterEach(() => { 24 | delete global.browser; 25 | browser._sandbox.reset(); 26 | }); 27 | 28 | describe('ports', () => { 29 | assert.strictEqual(mjs.ports instanceof Map, true, 'instance'); 30 | }); 31 | 32 | describe('remove port', () => { 33 | const func = mjs.removePort; 34 | beforeEach(() => { 35 | mjs.ports.clear(); 36 | }); 37 | afterEach(() => { 38 | mjs.ports.clear(); 39 | }); 40 | 41 | it('should get false', async () => { 42 | const res = await func(); 43 | assert.strictEqual(res, false, 'result'); 44 | }); 45 | 46 | it('should get false', async () => { 47 | mjs.ports.set('foo', {}); 48 | const res = await func('bar'); 49 | assert.strictEqual(mjs.ports.size, 1, 'size'); 50 | assert.strictEqual(res, false, 'result'); 51 | }); 52 | 53 | it('should get true', async () => { 54 | mjs.ports.set('foo', {}); 55 | const res = await func('foo'); 56 | assert.strictEqual(mjs.ports.size, 0, 'size'); 57 | assert.strictEqual(res, true, 'result'); 58 | }); 59 | }); 60 | 61 | describe('port on disconnect', () => { 62 | const func = mjs.portOnDisconnect; 63 | beforeEach(() => { 64 | mjs.ports.clear(); 65 | }); 66 | afterEach(() => { 67 | mjs.ports.clear(); 68 | }); 69 | 70 | it('should get result', async () => { 71 | const res = await func(); 72 | assert.deepEqual(res, [false], 'result'); 73 | }); 74 | 75 | it('should get result', async () => { 76 | mjs.ports.set('foo', {}); 77 | const res = await func({ 78 | name: 'bar' 79 | }); 80 | assert.strictEqual(mjs.ports.size, 1, 'size'); 81 | assert.deepEqual(res, [false], 'result'); 82 | }); 83 | 84 | it('should get result', async () => { 85 | const stubError = sinon.stub(console, 'error'); 86 | mjs.ports.set('foo', {}); 87 | const res = await func({ 88 | name: 'foo' 89 | }); 90 | const { called: errCalled } = stubError; 91 | stubError.restore(); 92 | assert.strictEqual(errCalled, false, 'error called'); 93 | assert.strictEqual(mjs.ports.size, 0, 'size'); 94 | assert.deepEqual(res, [true], 'result'); 95 | }); 96 | 97 | it('should log error', async () => { 98 | const stubError = sinon.stub(console, 'error'); 99 | mjs.ports.set('foo', {}); 100 | const res = await func({ 101 | error: new Error('error'), 102 | name: 'foo' 103 | }); 104 | const { calledOnce: errCalled } = stubError; 105 | stubError.restore(); 106 | assert.strictEqual(errCalled, true, 'error called'); 107 | assert.strictEqual(mjs.ports.size, 0, 'size'); 108 | assert.deepEqual(res, [true, false], 'result'); 109 | }); 110 | }); 111 | 112 | describe('add port', () => { 113 | const func = mjs.addPort; 114 | beforeEach(() => { 115 | mjs.ports.clear(); 116 | }); 117 | afterEach(() => { 118 | mjs.ports.clear(); 119 | }); 120 | 121 | it('should get null', async () => { 122 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 123 | id: browser.windows.WINDOW_ID_NONE 124 | }); 125 | const stubConnect = browser.runtime.connect; 126 | const res = await func(); 127 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 128 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 129 | assert.strictEqual(mjs.ports.size, 0, 'size'); 130 | assert.strictEqual(res, null, 'result'); 131 | }); 132 | 133 | it('should get port object', async () => { 134 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 135 | id: 1 136 | }); 137 | const stubConnect = browser.runtime.connect; 138 | const res = await func(); 139 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 140 | assert.strictEqual(stubConnect.calledOnce, true, 'called connect'); 141 | assert.strictEqual(mjs.ports.size, 1, 'size'); 142 | assert.strictEqual(res.name, `${SIDEBAR}_1`, 'name'); 143 | }); 144 | 145 | it('should get port object', async () => { 146 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 147 | id: 1 148 | }); 149 | const stubConnect = browser.runtime.connect; 150 | const res = await func(`${SIDEBAR}_1`); 151 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 152 | assert.strictEqual(stubConnect.calledOnce, true, 'called connect'); 153 | assert.strictEqual(mjs.ports.size, 1, 'size'); 154 | assert.strictEqual(res.name, `${SIDEBAR}_1`, 'name'); 155 | }); 156 | 157 | it('should get port object', async () => { 158 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 159 | id: 1 160 | }); 161 | const stubConnect = browser.runtime.connect; 162 | mjs.ports.set(`${SIDEBAR}_1`, { 163 | name: `${SIDEBAR}_1` 164 | }); 165 | const res = await func(); 166 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 167 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 168 | assert.strictEqual(mjs.ports.size, 1, 'size'); 169 | assert.strictEqual(res.name, `${SIDEBAR}_1`, 'name'); 170 | }); 171 | 172 | it('should get port object', async () => { 173 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 174 | id: 1 175 | }); 176 | const stubConnect = browser.runtime.connect; 177 | mjs.ports.set(`${SIDEBAR}_1`, { 178 | name: `${SIDEBAR}_1` 179 | }); 180 | const res = await func(`${SIDEBAR}_1`); 181 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 182 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 183 | assert.strictEqual(mjs.ports.size, 1, 'size'); 184 | assert.strictEqual(res.name, `${SIDEBAR}_1`, 'name'); 185 | }); 186 | }); 187 | 188 | describe('get port', () => { 189 | const func = mjs.getPort; 190 | beforeEach(() => { 191 | mjs.ports.clear(); 192 | }); 193 | afterEach(() => { 194 | mjs.ports.clear(); 195 | }); 196 | 197 | it('should get null', async () => { 198 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 199 | id: browser.windows.WINDOW_ID_NONE 200 | }); 201 | const stubConnect = browser.runtime.connect; 202 | const res = await func(); 203 | assert.strictEqual(stubCurrentWin.called, false, 'not called window'); 204 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 205 | assert.strictEqual(mjs.ports.size, 0, 'size'); 206 | assert.strictEqual(res, null, 'result'); 207 | }); 208 | 209 | it('should get null', async () => { 210 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 211 | id: browser.windows.WINDOW_ID_NONE 212 | }); 213 | const stubConnect = browser.runtime.connect; 214 | const res = await func(`${SIDEBAR}_1`, true); 215 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 216 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 217 | assert.strictEqual(mjs.ports.size, 0, 'size'); 218 | assert.strictEqual(res, null, 'result'); 219 | }); 220 | 221 | it('should get null', async () => { 222 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 223 | id: 1 224 | }); 225 | const stubConnect = browser.runtime.connect; 226 | const res = await func(`${SIDEBAR}_1`); 227 | assert.strictEqual(stubCurrentWin.called, false, 'not called window'); 228 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 229 | assert.strictEqual(mjs.ports.size, 0, 'size'); 230 | assert.strictEqual(res, null, 'result'); 231 | }); 232 | 233 | it('should get port object', async () => { 234 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 235 | id: 1 236 | }); 237 | const stubConnect = browser.runtime.connect; 238 | const res = await func(`${SIDEBAR}_1`, true); 239 | assert.strictEqual(stubCurrentWin.calledOnce, true, 'called window'); 240 | assert.strictEqual(stubConnect.calledOnce, true, 'called connect'); 241 | assert.strictEqual(mjs.ports.size, 1, 'size'); 242 | assert.strictEqual(res.name, `${SIDEBAR}_1`, 'name'); 243 | }); 244 | 245 | it('should get port object', async () => { 246 | const stubCurrentWin = browser.windows.getCurrent.resolves({ 247 | id: 1 248 | }); 249 | const stubConnect = browser.runtime.connect; 250 | mjs.ports.set(`${SIDEBAR}_1`, { 251 | name: `${SIDEBAR}_1` 252 | }); 253 | const res = await func(`${SIDEBAR}_1`); 254 | assert.strictEqual(stubCurrentWin.called, false, 'not called window'); 255 | assert.strictEqual(stubConnect.called, false, 'not called connect'); 256 | assert.strictEqual(mjs.ports.size, 1, 'size'); 257 | assert.strictEqual(res.name, `${SIDEBAR}_1`, 'name'); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "declarationDir": "types", 7 | "emitDeclarationOnly": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "newLine": "LF", 11 | "removeComments": true, 12 | "resolveJsonModule": true, 13 | "target": "esnext" 14 | }, 15 | "include": [ 16 | "index.js", 17 | "src/mjs/*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/scripts/commander.d.ts: -------------------------------------------------------------------------------- 1 | export function saveThemeManifest(dir: string, info: boolean): Promise; 2 | export function extractManifests(cmdOpts?: object): Promise; 3 | export function updateManifests(cmdOpts: object): Promise; 4 | export function saveLibraryPackage(lib: any[], info: boolean): Promise; 5 | export function extractLibraries(cmdOpts?: object): Promise; 6 | export function includeLibraries(cmdOpts: object): Promise; 7 | export function cleanDirectory(cmdOpts?: object): void; 8 | export function parseCommand(args: any[]): void; 9 | export { commander }; 10 | import { program as commander } from 'commander'; 11 | -------------------------------------------------------------------------------- /types/scripts/common.d.ts: -------------------------------------------------------------------------------- 1 | export function throwErr(e: object): never; 2 | export function logErr(e: object): boolean; 3 | export function logWarn(msg: any): boolean; 4 | export function logMsg(msg: any): any; 5 | export function getType(o: any): string; 6 | export function isString(o: any): boolean; 7 | -------------------------------------------------------------------------------- /types/scripts/file-util.d.ts: -------------------------------------------------------------------------------- 1 | export function getStat(file: string): object; 2 | export function isDir(dir: string): boolean; 3 | export function isFile(file: string): boolean; 4 | export function removeDir(dir: string): void; 5 | export function readFile(file: string, opt?: { 6 | encoding?: string; 7 | flag?: string; 8 | }): Promise; 9 | export function createFile(file: string, value: string): Promise; 10 | export function fetchText(url: string): Promise; 11 | -------------------------------------------------------------------------------- /types/src/lib/color/css-color.min.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace F1 { 2 | export { v1 as colorToHex }; 3 | export { b1 as colorToHsl }; 4 | export { w1 as colorToHwb }; 5 | export { $1 as colorToLab }; 6 | export { y1 as colorToLch }; 7 | export { N1 as colorToOklab }; 8 | export { E1 as colorToOklch }; 9 | export { C1 as colorToRgb }; 10 | export { rl as colorToXyz }; 11 | export { k1 as colorToXyzD50 }; 12 | export { g1 as numberToHex }; 13 | } 14 | declare function S1(t: any, e?: {}): any; 15 | declare function x1(t: any, e?: {}): boolean; 16 | declare function A0(t: any, e?: {}): any; 17 | declare namespace Qa { 18 | export { Gr as cssCalc }; 19 | export { xu as cssVar }; 20 | export { z0 as extractDashedIdent }; 21 | export { fn as isColor }; 22 | export { f1 as isGradient }; 23 | export { Ii as splitValue }; 24 | } 25 | declare function v1(t: any, e?: {}): any; 26 | declare function b1(t: any, e?: {}): any; 27 | declare function w1(t: any, e?: {}): any; 28 | declare function $1(t: any, e?: {}): any; 29 | declare function y1(t: any, e?: {}): any; 30 | declare function N1(t: any, e?: {}): any; 31 | declare function E1(t: any, e?: {}): any; 32 | declare function C1(t: any, e?: {}): any; 33 | declare function rl(t: any, e?: {}): any; 34 | declare function k1(t: any, e?: {}): any; 35 | declare function g1(t: any): any; 36 | declare function Gr(t: any, e?: {}): any; 37 | declare function xu(t: any, e?: {}): string | String; 38 | declare function z0(t: any): any; 39 | declare function fn(t: any, e?: {}): boolean; 40 | declare function f1(t: any, e?: {}): boolean; 41 | declare function Ii(t: any, e?: {}): any; 42 | export { F1 as convert, S1 as cssCalc, x1 as isColor, A0 as resolve, Qa as utils }; 43 | -------------------------------------------------------------------------------- /types/src/lib/tldts/index.esm.min.d.ts: -------------------------------------------------------------------------------- 1 | declare function d(n: any, t?: {}): any; 2 | declare function x(n: any, t?: {}): any; 3 | declare function c(n: any, t?: {}): any; 4 | declare function h(n: any, t?: {}): any; 5 | declare function m(n: any, t?: {}): any; 6 | declare function f(n: any, t?: {}): any; 7 | export { d as getDomain, x as getDomainWithoutSuffix, c as getHostname, h as getPublicSuffix, m as getSubdomain, f as parse }; 8 | -------------------------------------------------------------------------------- /types/src/lib/url/url-sanitizer-wo-dompurify.min.d.ts: -------------------------------------------------------------------------------- 1 | declare var he: { 2 | "__#2@#e": number; 3 | "__#2@#t": Set; 4 | replace(e: any): string | String; 5 | purify(e: any): string; 6 | sanitize(e: any, s: any): string; 7 | parse(e: any, s: any): { 8 | [k: string]: string | String; 9 | }; 10 | reset(): void; 11 | "__#1@#e": Set; 12 | get(): string[]; 13 | has(e: any): boolean; 14 | add(e: any): string[]; 15 | remove(e: any): boolean; 16 | verify(e: any): boolean; 17 | }; 18 | declare function de(t: any): Promise; 19 | declare function fe(t: any): boolean; 20 | declare function le(t: any): Promise<{ 21 | [k: string]: string | String; 22 | }>; 23 | declare function me(t: any): { 24 | [k: string]: string | String; 25 | }; 26 | declare function ce(t: any, e?: { 27 | allow: any[]; 28 | deny: any[]; 29 | only: any[]; 30 | }): Promise; 31 | declare function pe(t: any, e: any): string; 32 | export { he as default, de as isURI, fe as isURISync, le as parseURL, me as parseURLSync, ce as sanitizeURL, pe as sanitizeURLSync }; 33 | -------------------------------------------------------------------------------- /types/src/mjs/background-main.d.ts: -------------------------------------------------------------------------------- 1 | export const sidebar: Map; 2 | export function setSidebarState(windowId?: number): Promise; 3 | export function removeSidebarState(windowId: number): Promise; 4 | export function toggleSidebar(): Promise; 5 | export function handleSaveSessionRequest(domString: string, windowId: number): Promise; 6 | export function handleMsg(msg?: object): Promise; 7 | export function portOnMessage(msg: object): Promise; 8 | export function handleDisconnectedPort(port?: object): Promise; 9 | export function portOnDisconnect(port: object): Promise; 10 | export function handleConnectedPort(port?: object): Promise; 11 | export function handleCmd(cmd: string): Promise | null>; 12 | export { ports }; 13 | import { ports } from './port.js'; 14 | -------------------------------------------------------------------------------- /types/src/mjs/background.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/src/mjs/bookmark.d.ts: -------------------------------------------------------------------------------- 1 | export const folderMap: Map; 2 | export function createFolderMap(node?: string, recurse?: boolean): Promise; 3 | export function getFolderMap(recurse?: boolean): Promise; 4 | export function getBookmarkLocationId(): Promise; 5 | export function bookmarkTabs(nodes: any[], name?: string): Promise; 6 | -------------------------------------------------------------------------------- /types/src/mjs/browser-tabs.d.ts: -------------------------------------------------------------------------------- 1 | export function closeTabs(nodes: any[]): Promise | null>; 2 | export function closeDupeTabs(ids: any[], elm: object): Promise | null>; 3 | export function closeOtherTabs(nodes: any[]): Promise | null>; 4 | export function closeTabsToEnd(elm: object): Promise | null>; 5 | export function closeTabsToStart(elm: object): Promise | null>; 6 | export function createTabsInOrder(arr: any[], pop?: boolean): Promise | undefined; 7 | export function reopenTabsInContainer(nodes: any[], cookieId: string, windowId: number): Promise | null; 8 | export function dupeTab(tabId: number): Promise | null>; 9 | export function dupeTabs(nodes: any[]): Promise; 10 | export function highlightTabs(nodes: any[], opt?: { 11 | tabId: number; 12 | windowId: number; 13 | }): Promise | null>; 14 | export function moveTabsInOrder(arr: any[], windowId: number, pop?: boolean): Promise | undefined; 15 | export function moveTabsToEnd(nodes: any[], tabId: number, windowId: number): Promise; 16 | export function moveTabsToStart(nodes: any[], tabId: number, windowId: number): Promise; 17 | export function moveTabsToNewWindow(nodes: any[]): Promise | null>; 18 | export function muteTabs(nodes: any[], muted?: boolean): Promise; 19 | export function createNewTab(windowId?: number, opt?: object): Promise; 20 | export function createNewTabInContainer(cookieId: string, windowId?: number): Promise; 21 | export function pinTabs(nodes: any[], pinned?: boolean): Promise; 22 | export function reloadTabs(nodes: any[]): Promise; 23 | -------------------------------------------------------------------------------- /types/src/mjs/browser.d.ts: -------------------------------------------------------------------------------- 1 | export function isPermissionGranted(perm: object): Promise; 2 | export function createBookmark(opt: object): Promise; 3 | export function getBookmarkTreeNode(id?: string | any[]): Promise | null>; 4 | export function getCloseTabsByDoubleClickValue(): Promise; 5 | export function setContextMenuOnMouseup(): Promise; 6 | export function clearContextMenuOnMouseup(): Promise; 7 | export function getNewTabPositionValue(): Promise; 8 | export function isCommandCustomizable(): Promise; 9 | export function updateCommand(id: string, value?: string): Promise; 10 | export function getAllContextualIdentities(): Promise | null>; 11 | export function getContextualId(cookieStoreId: string): Promise; 12 | export function getEnabledTheme(): Promise | null>; 13 | export function getExtensionInfo(id: string): Promise; 14 | export function getExternalExtensions(): Promise | null>; 15 | export function clearNotification(id: string): Promise; 16 | export function createNotification(id: string, opt: object): Promise; 17 | export function removePermission(perm: string | any[]): Promise; 18 | export function requestPermission(perm: string | any[]): Promise; 19 | export function getManifestIcons(): object | string; 20 | export function getManifestVersion(): number; 21 | export function getOs(): Promise; 22 | export function makeConnection(id?: number | string, info?: object | boolean): Promise; 23 | export function sendMessage(id: number | string, msg: any, opt: object): Promise; 24 | export function isScriptingAvailable(): Promise; 25 | export function executeScriptToTab(opt?: object): Promise | null>; 26 | export function searchInNewTab(text: string, opt: object): Promise; 27 | export function searchWithSearchEngine(query: string, opt?: object): Promise; 28 | export function getRecentlyClosedTab(windowId?: number): Promise; 29 | export function getSessionWindowValue(key: string, windowId?: number): Promise; 30 | export function restoreSession(sessionId: string): Promise; 31 | export function setSessionWindowValue(key: string, value: string | object, windowId?: number): Promise; 32 | export function clearStorage(area?: string): Promise; 33 | export function getAllStorage(area?: string): Promise; 34 | export function getStorage(key: any, area?: string): Promise; 35 | export function removeStorage(key: any, area?: string): Promise; 36 | export function setStorage(obj: object, area?: string): Promise; 37 | export function createTab(opt?: object): Promise; 38 | export function duplicateTab(tabId: number, opt: object): Promise; 39 | export function queryTabs(opt: object): Promise>; 40 | export function execScriptToTab(tabId: number | object, opt?: object): Promise<(any[] | boolean) | null>; 41 | export function execScriptToTabs(opt?: object): Promise; 42 | export function execScriptsToTabInOrder(tabId: number | any[], opts?: any[]): Promise; 43 | export function getActiveTab(windowId?: number): Promise; 44 | export function getActiveTabId(windowId?: number): Promise; 45 | export function getAllTabsInWindow(windowId?: number): Promise>; 46 | export function getHighlightedTab(windowId?: number): Promise>; 47 | export function getTab(tabId: number): Promise; 48 | export function highlightTab(index: number | any[], windowId?: number): Promise; 49 | export function moveTab(tabId: number | any[], opt?: object): Promise | null>; 50 | export function reloadTab(tabId: number, opt: object): Promise; 51 | export function removeTab(arg: number | any[]): Promise; 52 | export function updateTab(tabId: number, opt: object): Promise; 53 | export function warmupTab(tabId: number): Promise; 54 | export function captureVisibleTab(windowId: number, opt: object): Promise; 55 | export function isTab(tabId: any): Promise; 56 | export function getCurrentTheme(windowId?: number): Promise; 57 | export function createNewWindow(opt: object): Promise; 58 | export function getAllNormalWindows(populate?: boolean): Promise>; 59 | export function getCurrentWindow(opt: object): Promise; 60 | export function getWindow(windowId: number, opt: object): Promise; 61 | export function checkIncognitoWindowExists(): Promise; 62 | -------------------------------------------------------------------------------- /types/src/mjs/color.d.ts: -------------------------------------------------------------------------------- 1 | export function convertRgbToHex(rgb: Array): string; 2 | export function getColorInHex(value: string, opt?: object): (string | any[]) | null; 3 | export function compositeLayeredColors(overlay: string, base: string): string; 4 | -------------------------------------------------------------------------------- /types/src/mjs/common.d.ts: -------------------------------------------------------------------------------- 1 | export function logErr(e: object): boolean; 2 | export function throwErr(e: object): never; 3 | export function logWarn(msg?: any): boolean; 4 | export function logMsg(msg?: any): object; 5 | export function getType(o: any): string; 6 | export function isString(o: any): boolean; 7 | export function isObjectNotEmpty(o: any): boolean; 8 | export function sleep(msec?: number, doReject?: boolean): Promise | null; 9 | export function addElementContentEditable(elm?: object, focus?: boolean): object; 10 | export function removeElementContentEditable(elm?: object): object; 11 | export function setElementDataset(elm?: object, key?: string, value?: string): object; 12 | -------------------------------------------------------------------------------- /types/src/mjs/localize.d.ts: -------------------------------------------------------------------------------- 1 | export function localizeAttr(elm?: object): Promise; 2 | export function localizeHtml(): Promise; 3 | -------------------------------------------------------------------------------- /types/src/mjs/main.d.ts: -------------------------------------------------------------------------------- 1 | export const userOpts: Map; 2 | export const userOptsKeys: Set; 3 | export function setUserOpts(opt?: object): Promise; 4 | export namespace sidebar { 5 | let context: any; 6 | let contextualIds: any; 7 | let duplicatedTabs: any; 8 | let firstSelectedTab: any; 9 | let incognito: boolean; 10 | let isMac: boolean; 11 | let lastClosedTab: any; 12 | let pinnedObserver: any; 13 | let pinnedTabsWaitingToMove: any; 14 | let tabsWaitingToMove: any; 15 | let windowId: any; 16 | } 17 | export function setSidebar(): Promise; 18 | export function initSidebar(bool?: boolean): Promise; 19 | export function setContext(elm?: object): void; 20 | export function setContextualIds(): Promise; 21 | export function setLastClosedTab(tab?: object): Promise; 22 | export function getLastClosedTab(): Promise; 23 | export function undoCloseTab(): Promise | null>; 24 | export function setPinnedTabsWaitingToMove(arr?: any[] | null): Promise; 25 | export function setTabsWaitingToMove(arr?: any[] | null): Promise; 26 | export function applyUserStyle(): Promise; 27 | export function applyUserCustomTheme(): Promise | null>; 28 | export function applyPinnedContainerHeight(entries: any[]): Promise | null; 29 | export function triggerDndHandler(evt: object): Function | null; 30 | export function handleCreateNewTab(evt: object): Promise | null; 31 | export function activateClickedTab(elm: object): Promise | null>; 32 | export function handleClickedTab(evt: object): Promise; 33 | export function addTabClickListener(elm?: object): Promise; 34 | export function toggleTabDblClickListener(elm?: object, bool?: boolean): Promise; 35 | export function replaceTabDblClickListeners(bool?: boolean): Promise; 36 | export function triggerTabWarmup(evt: object): Promise | null; 37 | export function addTabEventListeners(elm?: object): Promise; 38 | export function handleActivatedTab(info: object): Promise | null>; 39 | export function handleCreatedTab(tabsTab: object, opt?: object): Promise; 40 | export function handleAttachedTab(tabId: number, info: object): Promise | null>; 41 | export function handleDetachedTab(tabId: number, info: object): Promise; 42 | export function handleHighlightedTab(info: object): Promise; 43 | export function handleMovedTab(tabId: number, info: object): Promise | null>; 44 | export function handleRemovedTab(tabId: number, info: object): Promise; 45 | export function handleUpdatedTab(tabId: number, info?: object, tabsTab?: object): Promise; 46 | export function handleClickedMenu(info: object): Promise; 47 | export function prepareContexualIdsMenuItems(parentId: string, cookieId?: string): Promise; 48 | export function prepareNewTabMenuItems(elm: object): Promise; 49 | export function preparePageMenuItems(opt?: object): Promise; 50 | export function prepareTabGroupMenuItems(elm?: object, opt?: object): Promise; 51 | export function prepareTabMenuItems(elm: object): Promise; 52 | export function handleUpdatedTheme(info?: object): Promise | null>; 53 | export function handleInitCustomThemeRequest(remove?: boolean): Promise | null>; 54 | export function handleEvt(evt: object): Promise; 55 | export function handleContextmenuEvt(evt: object): Promise; 56 | export function handleWheelEvt(evt: object): Promise | null; 57 | export function handleMsg(msg: object): Promise; 58 | export function requestSidebarStateUpdate(): Promise | null>; 59 | export function setStorageValue(item?: string, obj?: object, changed?: boolean): Promise; 60 | export function handleStorage(data?: object, area?: string, changed?: boolean): Promise; 61 | export function restoreHighlightedTabs(): Promise; 62 | export function restoreTabGroups(): Promise; 63 | export function emulateTabsInOrder(arr: any[]): Promise; 64 | export function emulateTabs(): Promise; 65 | export function setPinnedObserver(): Promise; 66 | export function setMain(): Promise; 67 | export function startup(): Promise; 68 | export { ports }; 69 | import { ports } from './port.js'; 70 | -------------------------------------------------------------------------------- /types/src/mjs/menu.d.ts: -------------------------------------------------------------------------------- 1 | export const menuItemMap: Map; 2 | export function updateContextMenu(menuItemId?: string, data?: object): Promise; 3 | export function createMenuItemCallback(): Promise; 4 | export function createMenuItem(data?: object): Promise<(string | number) | null>; 5 | export function createContextualIdentitiesMenu(info?: object): Promise; 6 | export function createContextMenu(menu?: object, parentId?: string): Promise; 7 | export function updateContextualIdentitiesMenu(info?: object): Promise; 8 | export function removeContextualIdentitiesMenu(info?: object): Promise; 9 | export function restoreContextMenu(): Promise; 10 | export function overrideContextMenu(opt?: object): Promise; 11 | -------------------------------------------------------------------------------- /types/src/mjs/options-main.d.ts: -------------------------------------------------------------------------------- 1 | export function sendMsg(msg: any): Promise; 2 | export function initExt(init?: boolean): Promise | null>; 3 | export function initCustomTheme(init?: boolean): Promise | null>; 4 | export function requestCustomTheme(bool?: boolean): Promise | null>; 5 | export function storeCustomTheme(): Promise; 6 | export function createPref(elm?: object): Promise; 7 | export function storePref(evt: object): Promise; 8 | export function toggleSubItems(evt: object): void; 9 | export function toggleCustomThemeSettings(): void; 10 | export function addCustomThemeListener(): Promise; 11 | export function setCustomThemeValue(obj?: object): Promise; 12 | export function addBookmarkLocations(): Promise; 13 | export function handleInitCustomThemeClick(evt: object): Promise; 14 | export function addInitCustomThemeListener(): Promise; 15 | export function handleInitExtClick(evt: object): Promise; 16 | export function addInitExtensionListener(): Promise; 17 | export function saveUserCss(): Promise | null; 18 | export function addUserCssListener(): Promise; 19 | export function handleInputChange(evt: object): Promise; 20 | export function addInputChangeListener(): Promise; 21 | export function setHtmlInputValue(data?: object): Promise; 22 | export function setValuesFromStorage(): Promise; 23 | export function handleMsg(msg: object): Promise; 24 | export { folderMap } from "./bookmark.js"; 25 | -------------------------------------------------------------------------------- /types/src/mjs/options.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/src/mjs/port.d.ts: -------------------------------------------------------------------------------- 1 | export const ports: Map; 2 | export function removePort(portId: string): Promise; 3 | export function portOnDisconnect(port?: object): Promise; 4 | export function addPort(portId?: string): Promise; 5 | export function getPort(portId?: string, add?: boolean): Promise; 6 | -------------------------------------------------------------------------------- /types/src/mjs/session.d.ts: -------------------------------------------------------------------------------- 1 | export function getSessionTabList(key: string, windowId?: number): Promise; 2 | export const mutex: Set; 3 | export function saveSessionTabList(domStr: string, windowId: number): Promise; 4 | export function requestSaveSession(): Promise; 5 | export { ports } from "./port.js"; 6 | -------------------------------------------------------------------------------- /types/src/mjs/sidebar.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/src/mjs/tab-content.d.ts: -------------------------------------------------------------------------------- 1 | export const favicon: Map; 2 | export const contextualIdentitiesIconName: Set; 3 | export const contextualIdentitiesIconColor: Set; 4 | export function tabIconFallback(evt?: object): boolean; 5 | export function addTabIconErrorListener(elm?: object): Promise; 6 | export function setTabIcon(elm?: object, info?: object): Promise; 7 | export function setTabContent(tab?: object, tabsTab?: object): Promise; 8 | export function handleClickedTabAudio(elm?: object): Promise | null>; 9 | export function tabAudioOnClick(evt: object): Promise | null; 10 | export function addTabAudioClickListener(elm?: object): Promise; 11 | export function setTabAudio(elm?: object, info?: object, num?: number): Promise; 12 | export function setTabAudioIcon(elm?: object, info?: object): Promise; 13 | export function setCloseTab(elm?: object, highlighted?: boolean, num?: number): Promise; 14 | export function tabCloseOnClick(evt: object): Promise | null; 15 | export function preventDefaultEvent(evt: object): void; 16 | export function addTabCloseClickListener(elm?: object): Promise; 17 | export function setContextualIdentitiesIcon(elm?: object, info?: object): Promise; 18 | export function addHighlight(elm: object, num?: number): Promise; 19 | export function addHighlightToTabs(tabIds: any[]): Promise; 20 | export function removeHighlight(elm: object): Promise; 21 | export function removeHighlightFromTabs(tabIds: any[]): Promise; 22 | -------------------------------------------------------------------------------- /types/src/mjs/tab-dnd.d.ts: -------------------------------------------------------------------------------- 1 | export function clearDropTarget(): void; 2 | export function createDroppedTextTabsInOrder(opts?: Array, pop?: boolean): Promise | undefined; 3 | export function handleDroppedText(target: object, data: object): Promise | null; 4 | export function handleDroppedTabs(target: object, data: object): Promise | null; 5 | export function handleDrop(evt: object, opt?: { 6 | windowId: boolean; 7 | }): (Promise | undefined) | null; 8 | export function handleDragEnd(evt: object): void; 9 | export function handleDragLeave(evt: object): void; 10 | export function handleDragOver(evt: object, opt?: { 11 | isMac: boolean; 12 | windowId: boolean; 13 | }): void; 14 | export function handleDragStart(evt: object, opt?: { 15 | isMac: boolean; 16 | windowId: boolean; 17 | }): Promise | undefined; 18 | export { ports } from "./session.js"; 19 | -------------------------------------------------------------------------------- /types/src/mjs/tab-group.d.ts: -------------------------------------------------------------------------------- 1 | export function restoreTabContainers(): Promise; 2 | export function collapseTabGroup(elm?: object, activate?: boolean): void; 3 | export function expandTabGroup(elm?: object): void; 4 | export function toggleTabGrouping(): Promise; 5 | export function toggleTabGroupCollapsedState(elm?: object, activate?: boolean): Promise; 6 | export function toggleTabGroupsCollapsedState(elm?: object): Promise; 7 | export function collapseTabGroups(elm?: object): Promise; 8 | export function getTabGroupHeading(node: object): object; 9 | export function handleTabGroupCollapsedState(evt: object): Promise; 10 | export function handleTabGroupsCollapsedState(evt: object): Promise; 11 | export function addTabContextClickListener(elm?: object, multi?: boolean): void; 12 | export function replaceTabContextClickListener(multi?: boolean): Promise; 13 | export function expandActivatedCollapsedTab(): Promise | null>; 14 | export function finishGroupLabelEdit(evt: object): Promise; 15 | export function startGroupLabelEdit(node: object): Promise; 16 | export function enableGroupLabelEdit(evt: object): Promise; 17 | export function triggerDndHandler(evt: object): Function | null; 18 | export function addListenersToHeadingItems(node: object, opt?: { 19 | isMac?: boolean; 20 | multi?: boolean; 21 | windowId?: number; 22 | }): Promise; 23 | export function removeListenersFromHeadingItems(node: object): Promise; 24 | export function toggleTabGroupHeadingState(node: object, opt?: object): Promise; 25 | export function toggleAutoCollapsePinnedTabs(auto?: boolean): Promise; 26 | export function bookmarkTabGroup(node: object): Promise | null>; 27 | export function selectTabGroup(node: object): Promise | null>; 28 | export function closeTabGroup(node: object): Promise | null>; 29 | export function detachTabsFromGroup(nodes: any[], windowId?: number): Promise | null; 30 | export function groupSelectedTabs(windowId?: number): Promise | null; 31 | export function groupSameContainerTabs(tabId: number, windowId?: number): Promise | null; 32 | export function groupSameDomainTabs(tabId: number, windowId?: number): Promise | null; 33 | export function ungroupTabs(node?: object): Promise; 34 | export { ports } from "./session.js"; 35 | -------------------------------------------------------------------------------- /types/src/mjs/util.d.ts: -------------------------------------------------------------------------------- 1 | export function getTemplate(id: string): object; 2 | export function getSidebarTabContainer(node?: object): object; 3 | export function restoreTabContainer(container?: object): void; 4 | export function createSidebarTab(node?: object, target?: object): object; 5 | export function getSidebarTab(node?: object): object; 6 | export function getSidebarTabId(node?: object): number | null; 7 | export function getSidebarTabIds(nodes: any[]): any[]; 8 | export function getSidebarTabIndex(node: object): number | null; 9 | export function getTabsInRange(tabA: object, tabB: object): any[]; 10 | export function getNextTab(elm: object, skipCollapsed?: boolean): object; 11 | export function getPreviousTab(elm: object, skipCollapsed?: boolean): object; 12 | export function isNewTab(node?: object): boolean; 13 | export function activateTab(elm: object): Promise | null; 14 | export function scrollTabIntoView(elm?: object): Promise; 15 | export function switchTab(opt?: object): Promise | null; 16 | export function createUrlMatchString(url: string): string; 17 | export function storeCloseTabsByDoubleClickValue(bool?: boolean): Promise; 18 | --------------------------------------------------------------------------------