├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── improvement.md ├── screenshots │ ├── editor.png │ ├── installer.png │ ├── manager-config.png │ ├── manager.png │ ├── options.png │ ├── popup-config.png │ └── popup-search.png └── workflows │ └── ci.yml ├── .gitignore ├── .tx └── config ├── BUILD.md ├── LICENSE ├── README.md ├── babel.config.js ├── eslint.config.js ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── privacy-policy.md ├── src ├── .types.d.ts ├── _locales │ ├── ar │ │ └── messages.json │ ├── bg │ │ └── messages.json │ ├── cs │ │ └── messages.json │ ├── da │ │ └── messages.json │ ├── de │ │ └── messages.json │ ├── el │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── en_GB │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── es_419 │ │ └── messages.json │ ├── et │ │ └── messages.json │ ├── fa │ │ └── messages.json │ ├── fi │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── he │ │ └── messages.json │ ├── hu │ │ └── messages.json │ ├── it │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── ko │ │ └── messages.json │ ├── lv │ │ └── messages.json │ ├── nl │ │ └── messages.json │ ├── pl │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── pt_PT │ │ └── messages.json │ ├── ro │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── sr │ │ └── messages.json │ ├── sv │ │ └── messages.json │ ├── te │ │ └── messages.json │ ├── tr │ │ └── messages.json │ ├── uk │ │ └── messages.json │ ├── vi │ │ └── messages.json │ ├── zh_CN │ │ └── messages.json │ └── zh_TW │ │ └── messages.json ├── background.html ├── background │ ├── broadcast-injector-config.js │ ├── broadcast.js │ ├── browser-cmd-hotkeys.js │ ├── color-scheme.js │ ├── common.js │ ├── content-scripts.js │ ├── context-menus.js │ ├── db-chrome-storage.js │ ├── db-to-cloud-broker.js │ ├── db.js │ ├── download.js │ ├── icon-manager.js │ ├── index.js │ ├── intro.js │ ├── navigation-manager.js │ ├── offscreen.js │ ├── popup-data.js │ ├── prefs-api.js │ ├── set-client-data.js │ ├── style-manager │ │ ├── cache-builder.js │ │ ├── cache.js │ │ ├── fixer.js │ │ ├── index.js │ │ ├── init.js │ │ ├── matcher.js │ │ └── util.js │ ├── style-search-db.js │ ├── style-via-api.js │ ├── style-via-webrequest.js │ ├── sw │ │ ├── index.js │ │ └── keep-alive.js │ ├── sync-manager.js │ ├── tab-manager.js │ ├── tab-util.js │ ├── token-manager.js │ ├── update-manager.js │ ├── usercss-install-helper.js │ ├── usercss-manager.js │ ├── usercss-template.js │ ├── uso-api.js │ ├── usw-api.js │ └── util.js ├── cm │ ├── index.css │ ├── index.js │ ├── jsonlint-bundle.js │ ├── themes.js │ └── util.js ├── content │ ├── apply.js │ ├── install-hook-greasyfork.js │ ├── install-hook-usercss.js │ ├── install-hook-userstyles.js │ ├── install-hook-userstylesworld.js │ └── style-injector.js ├── css │ ├── global-dark.css │ ├── global.css │ ├── icons.ttf │ ├── onoffswitch.css │ └── spinner.css ├── edit.html ├── edit │ ├── applies-to.html │ ├── autocomplete-util.js │ ├── autocomplete.css │ ├── autocomplete.js │ ├── beautify.js │ ├── codemirror-factory.js │ ├── colorpicker-helper.js │ ├── compact-header.js │ ├── dirty-reporter.js │ ├── drafts.js │ ├── edit.css │ ├── editor-header.js │ ├── editor-settings.html │ ├── editor.js │ ├── embedded-popup.js │ ├── global-search.css │ ├── global-search.html │ ├── global-search.js │ ├── index.js │ ├── keymap-help.html │ ├── keymap-help.js │ ├── lazy-init.js │ ├── linter │ │ ├── defaults.js │ │ ├── dialogs.js │ │ ├── engines.js │ │ ├── index.js │ │ ├── reports.html │ │ ├── reports.js │ │ └── store.js │ ├── live-preview.js │ ├── load-style.js │ ├── moz-section-finder.js │ ├── moz-section-widget.js │ ├── on-msg-extension.js │ ├── regexp-tester.css │ ├── regexp-tester.js │ ├── sections-editor-section.js │ ├── sections-editor.html │ ├── sections-editor.js │ ├── settings.css │ ├── settings.js │ ├── source-editor.js │ ├── style-settings.html │ ├── unload.js │ ├── usw-integration.js │ ├── util.js │ └── windowed-mode.js ├── icon │ ├── 128.png │ ├── 16.png │ ├── 16w.png │ ├── 16x.png │ ├── 19.png │ ├── 19w.png │ ├── 19x.png │ ├── 32.png │ ├── 32w.png │ ├── 32x.png │ ├── 38.png │ ├── 38w.png │ ├── 38x.png │ ├── 48.png │ ├── eyedropper │ │ ├── 16px.png │ │ ├── 32px.png │ │ └── README.md │ └── light │ │ ├── 16.png │ │ ├── 16w.png │ │ ├── 16x.png │ │ ├── 19.png │ │ ├── 19w.png │ │ ├── 19x.png │ │ ├── 32.png │ │ ├── 32w.png │ │ ├── 32x.png │ │ ├── 38.png │ │ ├── 38w.png │ │ └── 38x.png ├── icons │ ├── check1.svg │ ├── check2.svg │ ├── checked.svg │ ├── close.svg │ ├── cloud.svg │ ├── config.svg │ ├── copy.svg │ ├── edit.svg │ ├── empty.svg │ ├── external.svg │ ├── info.svg │ ├── install.svg │ ├── log.svg │ ├── menu.svg │ ├── minus.svg │ ├── plus.svg │ ├── reorder.svg │ ├── select-arrow.svg │ ├── sort-down.svg │ ├── undo.svg │ ├── update-check.svg │ ├── usercss.svg │ └── v.svg ├── install-usercss.html ├── install-usercss │ ├── direct-downloader.js │ ├── index.js │ ├── install-usercss.css │ └── port-downloader.js ├── js │ ├── browser.js │ ├── chrome-sync.js │ ├── cmpver.js │ ├── color │ │ ├── LICENSE │ │ ├── README.md │ │ ├── color-converter.js │ │ ├── color-mimicry.js │ │ ├── color-picker.css │ │ ├── color-picker.js │ │ └── color-view.js │ ├── consts.js │ ├── dlg │ │ ├── config-dialog.css │ │ ├── config-dialog.js │ │ ├── message-box.css │ │ └── message-box.js │ ├── dnr.js │ ├── dom-init.js │ ├── dom-on-load.js │ ├── dom-util.js │ ├── dom.js │ ├── get-client-data.js │ ├── header-resizer.js │ ├── localization.js │ ├── meta-parser.js │ ├── moz-parser.js │ ├── msg-api.js │ ├── msg-init.js │ ├── msg.js │ ├── port.js │ ├── prefs.js │ ├── sections-util.js │ ├── storage-util.js │ ├── sync-util.js │ ├── themer.js │ ├── ua.js │ ├── urls.js │ ├── usercss-compiler.js │ ├── util-webext.js │ ├── util.js │ ├── worker-background.js │ ├── worker-editor.js │ ├── worker-util.js │ └── worker.js ├── manage.html ├── manage │ ├── events.js │ ├── filters.js │ ├── import-export.js │ ├── incremental-search.js │ ├── index.js │ ├── injection-order.css │ ├── injection-order.js │ ├── lazy-init.js │ ├── manage-newui.css │ ├── manage.css │ ├── new-ui.js │ ├── render-favs.js │ ├── render.js │ ├── router.js │ ├── sorter.js │ ├── updater-ui.js │ └── util.js ├── manifest-mv2.json ├── manifest-mv3.json ├── manifest.json ├── offscreen.html ├── offscreen │ └── index.js ├── options.html ├── options │ ├── index.js │ ├── options-sync.js │ ├── options.css │ └── shortcuts-ff.html ├── popup.html ├── popup │ ├── events.js │ ├── hotkeys.js │ ├── index.js │ ├── popup.css │ ├── search.css │ ├── search.html │ └── search.js └── vendor-overwrites │ ├── beautify │ ├── LICENSE │ ├── README.md │ └── beautify-css-mod.js │ └── codemirror-addon │ └── match-highlighter.js ├── tools ├── build-icons.mjs ├── build-zip.js ├── chrome-api-no-cb.js ├── fix-transifex.js ├── shim │ ├── null.js │ ├── path.js │ └── url.js ├── sync-manifest-version.js ├── test-css.js ├── util.js ├── wp-cjs-to-esm-loader.js ├── wp-lzstring-loader.js ├── wp-raw-patch-plugin.js └── zip-id.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Enforce LF on files that are copied as-is either fully or partially (html attribute values) 5 | *.html text eol=lf 6 | *.json text eol=lf 7 | *.md text eol=lf 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Stylus 2 | 3 | 1. [Getting involved](#getting-involved) 4 | 2. [How to report issues](#how-to-report-issues) 5 | 3. [Adding translations](#adding-translations) 6 | 4. [Pull requests](#pull-requests) 7 | 5. [Scripts](#scripts) 8 | 6. [Updating locale files](#updating-locale-files-admin-only) 9 | 7. [Contact us](#contact-us) 10 | 11 | ## Getting involved 12 | 13 | There are a number of ways to get involved with the development of Stylus. Even if you've never contributed to an Open Source project before, we're always looking for help by identifying issues and suggesting improvements. 14 | 15 | ## How to report issues 16 | 17 | When an [**issue**](https://github.com/openstyles/stylus/issues) is opened, a template is provided. Please answer these questions as thoroughly as possible. If we were psychic, we'd be hanging out in casinos playing poker until they kicked us out. We can't read your mind! Please provide step-by-step directions on how to reproduce the issue as well as the versions of your operating system, browser and Stylus. 18 | 19 | When adding a **feature request**, please search through the existing issues to see if it the feature has already been requested, added or rejected. 20 | 21 | If not, then provide details describing which page the feature will effect, e.g. popup, manager or editor. Then describe the request and explain how you think it would benefit the user experience. 22 | 23 | 24 | ## Adding translations 25 | 26 | You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). 27 | Only the languages supported by the web store are allowed: 28 | https://developer.chrome.com/docs/webstore/i18n/#localeTable 29 | 30 | 31 | ## Pull requests 32 | 33 | * First open an issue to discuss your changes. 34 | * Then download, fork or clone this repository. 35 | 36 | * Use the provided `.editorconfig` file with your code editor. Don't know what that is? Then check out https://editorconfig.org/. 37 | * Make any changes within a branch of this repository (not the `master` branch). 38 | * Submit a pull request and include a reference to the initial issue with the discussion. 39 | 40 | ## Build scripts 41 | 42 | See [BUILD.md](../BUILD.md) for more information. 43 | 44 | ## Contact us 45 | 46 | If you prefer a more informal method of getting in touch or starting a conversation, please [join us on Discord](https://discordapp.com/widget?id=379521691774353408) or leave a comment in the [discussion section](https://add0n.com/stylus.html#reviews). We will monitor any discussions there and join in, and it may be a more appropriate venue for opinions and less urgent suggestions. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug/problem 3 | about: A bug or a problem with Stylus 4 | title: Short description 5 | labels: bug 6 | --- 7 | 8 | 15 | 16 | ### Description 17 | 1. 18 | 2. 19 | 3. 20 | 21 | 22 | 23 | ### System Information 24 | 25 | - OS: 26 | - Browser: 27 | - Stylus Version: 28 | 29 | ### Screenshots, links, CSS 30 | 31 | 32 | 33 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement/suggestion 3 | about: An idea you'd like to see implemented in Stylus 4 | title: Short description 5 | --- 6 | 7 | 13 | -------------------------------------------------------------------------------- /.github/screenshots/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/editor.png -------------------------------------------------------------------------------- /.github/screenshots/installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/installer.png -------------------------------------------------------------------------------- /.github/screenshots/manager-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/manager-config.png -------------------------------------------------------------------------------- /.github/screenshots/manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/manager.png -------------------------------------------------------------------------------- /.github/screenshots/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/options.png -------------------------------------------------------------------------------- /.github/screenshots/popup-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/popup-config.png -------------------------------------------------------------------------------- /.github/screenshots/popup-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/.github/screenshots/popup-search.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | paths: 5 | - 'src/**' 6 | - '*.js' # root configs 7 | - 'package.json' 8 | - '!src/types.d.ts' 9 | - .github/workflows/ci.yml 10 | 11 | pull_request: 12 | branches: [master] 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: pnpm/action-setup@v4 21 | with: 22 | version: 9 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '>=22.12.0' 26 | 27 | - run: pnpm install 28 | - run: pnpm test 29 | 30 | - run: echo "_REV=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 31 | 32 | # MV3 chrome 33 | 34 | - run: rm -rf dist && pnpm build-chrome-mv3 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: 'stylus-chrome-mv3-${{ env._VER }}-${{ env._REV }}' 38 | path: 'dist/*' 39 | if-no-files-found: error 40 | 41 | # MV2 firefox 42 | 43 | - run: rm -rf dist && pnpm build-firefox 44 | - uses: actions/upload-artifact@v4 45 | with: 46 | name: 'stylus-firefox-mv2-${{ env._VER }}-${{ env._REV }}' 47 | path: 'dist/*' 48 | if-no-files-found: error 49 | 50 | # MV2 51 | 52 | - run: rm -rf dist && pnpm build-mv2 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: 'stylus-mv2-${{ env._VER }}-${{ env._REV }}' 56 | path: 'dist/*' 57 | if-no-files-found: error 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.tmp* 3 | .DS_Store 4 | desktop.ini 5 | /.eslintcache 6 | /.vscode 7 | /.*.html 8 | /dist*/ 9 | /node_modules/ 10 | /yarn.lock 11 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:github-7:p:Stylus:r:messages] 5 | file_filter = src/_locales//messages.json 6 | minimum_perc = 0 7 | source_file = src/_locales/en/messages.json 8 | source_lang = en_US 9 | type = CHROME 10 | 11 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Build this project 2 | 3 | ## Preparation 4 | 5 | 1. Install [Node.js](https://nodejs.org/en/). 6 | 2. Install PNPM e.g. run `npm install -g pnpm` or use another method for your OS, see https://pnpm.io/installation 7 | 3. Go to the project root, run `pnpm i`. This will install all required dependencies. 8 | 9 | ## Preparation for Transifex 10 | 11 | Extra preparations are needed if you want to pull locale files from Transifex: 12 | 13 | 1. Install Transifex client. Follow the instructions on [this page](https://docs.transifex.com/client/installing-the-client). 14 | 2. You need a `.transifexrc` file in the root folder. Contact another admin if you need one. It includes the API key to use Transifex's API. 15 | 16 | ## Build 17 | 18 | | type | command | 19 | |-----------------------|-------------------------| 20 | | MV2 for any browser | `pnpm build-mv2` | 21 | | MV2 Firefox optimized | `pnpm build-firefox` | 22 | | MV3 Chrome/Chromiums | `pnpm build-chrome-mv3` | 23 | 24 | ⚠ `dist` folder is not cleared. 25 | 26 | ## Watch / develop 27 | 28 | | type | command | 29 | |------|------------------| 30 | | MV2 | `pnpm watch-mv2` | 31 | | MV3 | `pnpm watch-mv3` | 32 | 33 | ⚠ `dist` folder is not cleared. 34 | 35 | ## Create ZIP files for an extension gallery 36 | 37 | The files are created in the project root directory. 38 | 39 | | type | command | 40 | |-----------------|----------------------------| 41 | | All | `pnpm zip` | 42 | | MV2 Firefox | `pnpm zip-firefox` | 43 | | MV2 Chrome | `pnpm zip-chrome-mv2` | 44 | | MV3 Chrome | `pnpm zip-chrome-mv3` | 45 | | MV3 Chrome beta | `pnpm zip-chrome-mv3-beta` | 46 | 47 | ## Tag a release/Bump the version 48 | 49 | | type | command | 50 | |----------|--------------------| 51 | | Beta/Dev | `pnpm bump` | 52 | | Stable | `pnpm bump-stable` | 53 | 54 | There are some scripts that will run automatically before/after tagging a version. Includes: 55 | 56 | 1. Test. 57 | 2. Update version number in `manifest.json`. 58 | 3. Generate the ZIP file. 59 | 4. Push the tag to github. 60 | 61 | ## Translation 62 | 63 | We host locale files (`message.json`) on Transifex. All the files exist in our GitHub repository, but if you need to update the locale files, you will need to install the [Transifex client](https://docs.transifex.com/client/installing-the-client) 64 | 65 | To pull files from Transifex, run 66 | 67 | ``` 68 | pnpm update-locales 69 | ``` 70 | 71 | To push files to Transifex: 72 | 73 | ``` 74 | pnpm update-transifex 75 | ``` 76 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {getBrowserlist} = require('./tools/util'); 4 | 5 | module.exports = { 6 | targets: getBrowserlist(), 7 | assumptions: { 8 | constantReexports: true, 9 | noDocumentAll: true, 10 | noIncompleteNsImportDetection: true, 11 | noNewArrows: true, 12 | objectRestNoSymbols: true, 13 | privateFieldsAsSymbols: true, 14 | }, 15 | presets: [ 16 | ['@babel/preset-env', { 17 | useBuiltIns: false, 18 | bugfixes: true, 19 | loose: true, 20 | modules: false, 21 | }], 22 | ], 23 | plugins: [ 24 | // '@babel/plugin-transform-runtime', 25 | // ['transform-modern-regexp', {useRe: true}], // TODO: use for complex regexps 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules"], 9 | "typeAcquisition": { 10 | "include": ["@/types.d.ts"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stylus", 3 | "version": "2.3.14", 4 | "description": "Redesign the web with Stylus, a user styles manager", 5 | "license": "GPL-3.0-only", 6 | "repository": "openstyles/stylus", 7 | "author": "Stylus Team", 8 | "dependencies": { 9 | "@eight04/draggable-list": "^0.3.0", 10 | "codemirror": "5.65.10", 11 | "csslint-mod": "github:openstyles/csslint-mod#semver:^v1.0.2", 12 | "db-to-cloud": "^0.7.0", 13 | "less": "^4.2.2", 14 | "lz-string-unsafe": "1.4.4-fork-1", 15 | "stylelint-bundle": "^16.5.0", 16 | "stylus-lang-bundle": "^0.64.0", 17 | "usercss-meta": "^0.12.0", 18 | "webext-launch-web-auth-flow": "^0.1.2" 19 | }, 20 | "devDependencies": { 21 | "@automattic/webpack-inline-constant-exports-plugin": "^1.0.0", 22 | "@babel/core": "^7.26.0", 23 | "@babel/plugin-transform-runtime": "^7.25.9", 24 | "@babel/preset-env": "^7.26.0", 25 | "@types/chrome": "^0.0.299", 26 | "@types/codemirror": "^5.60.15", 27 | "@types/firefox-webext-browser": "^120.0.4", 28 | "@types/webpack": "^5.28.5", 29 | "autoprefixer": "^10.4.20", 30 | "babel-loader": "^9.2.1", 31 | "chalk": "^4.1.2", 32 | "chrome-types": "^0.1.334", 33 | "copy-webpack-plugin": "^12.0.2", 34 | "css-loader": "^7.1.2", 35 | "css-minimizer-webpack-plugin": "^7.0.0", 36 | "deepmerge": "^4.3.1", 37 | "eslint": "^9.17.0", 38 | "fast-glob": "^3.3.2", 39 | "fs-extra": "^11.2.0", 40 | "globals": "^15.14.0", 41 | "html-loader": "^5.1.0", 42 | "html-webpack-plugin": "^5.6.3", 43 | "html-webpack-processing-plugin": "^1.0.2", 44 | "jszip": "^3.10.1", 45 | "mini-css-extract-plugin": "^2.9.2", 46 | "postcss": "^8.4.49", 47 | "postcss-calc": "^10.0.2", 48 | "postcss-loader": "^8.1.1", 49 | "postcss-nested": "^6.2.0", 50 | "postcss-preset-env": "^9.6.0", 51 | "postcss-simple-vars": "^7.0.1", 52 | "svg2ttf": "^6.0.3", 53 | "svgicons2svgfont": "^15.0.0", 54 | "terser-webpack-plugin": "^5.3.11", 55 | "webpack": "^5.97.1", 56 | "webpack-bundle-analyzer": "^4.10.2", 57 | "webpack-cli": "^5.1.4" 58 | }, 59 | "scripts": { 60 | "lint": "eslint . --cache", 61 | "test": "pnpm lint && pnpm test-csslint", 62 | "test-csslint": "node tools/test-css.js", 63 | "update-locales": "tx pull --all --minimum-perc=1 && node tools/fix-transifex.js commit", 64 | "update-transifex": "tx push -s", 65 | "build-icons": "node tools/build-icons.mjs", 66 | "build-mv2": "webpack-cli --node-env any-mv2", 67 | "build-chrome-mv3": "webpack-cli --node-env chrome-mv3", 68 | "build-firefox": "webpack-cli --node-env firefox", 69 | "watch-mv2": "webpack-cli watch --node-env any-mv2", 70 | "watch-mv3": "webpack-cli watch --node-env chrome-mv3", 71 | "watch-firefox": "webpack-cli watch --node-env firefox", 72 | "zip": "pnpm test && node tools/build-zip.js", 73 | "zip-chrome-mv2": "pnpm test && node tools/build-zip.js chrome-mv2", 74 | "zip-chrome-mv3": "pnpm test && node tools/build-zip.js chrome-mv3", 75 | "zip-chrome-mv3-beta": "pnpm test && node tools/build-zip.js chrome-mv3-beta", 76 | "zip-firefox": "pnpm test && node tools/build-zip.js firefox", 77 | "preversion": "pnpm test", 78 | "version": "node tools/sync-manifest-version.js && git add .", 79 | "bump": "pnpm version patch", 80 | "bump-stable": "pnpm version minor" 81 | }, 82 | "engines": { 83 | "node": ">=22.12.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {getBrowserlist} = require('./tools/util'); 4 | 5 | module.exports = { 6 | plugins: [ 7 | ['postcss-preset-env', { 8 | browsers: getBrowserlist(), 9 | features: { 10 | 'clamp': false, // used intentionally with a fallback 11 | 'prefers-color-scheme-query': false, // we manually handle it via cssRules 12 | }, 13 | }], 14 | 'postcss-simple-vars', 15 | 'postcss-nested', 16 | 'autoprefixer', 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Unlike other similar extensions, we don't find you to be all that interesting. Your questionable browsing history should remain between you and the NSA. Stylus collects nothing. Period. 4 | 5 | Again, **no data or personal information is collected by Stylus**. 6 | 7 | ## Contact 8 | 9 | If you have any questions or suggestions regarding this privacy policy, do not hesitate to [contact us](stylus.openstyles@gmail.com). 10 | -------------------------------------------------------------------------------- /src/_locales/en_GB/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appliesRemoveError": { 3 | "message": "Cannot remove last 'applies to' entry" 4 | }, 5 | "checkAllUpdatesForce": { 6 | "message": "Check again—I didn't edit any styles!" 7 | }, 8 | "cm_autoCloseBrackets": { 9 | "message": "Auto-close brackets and quotes" 10 | }, 11 | "cm_colorpicker": { 12 | "message": "Colour pickers for CSS colours" 13 | }, 14 | "cm_resizeGripHint": { 15 | "message": "Double-click to maximise/restore the height" 16 | }, 17 | "colorpickerTooltip": { 18 | "message": "Open colour picker" 19 | }, 20 | "description": { 21 | "message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites." 22 | }, 23 | "editGotoLine": { 24 | "message": "Go to line (or line:col)" 25 | }, 26 | "editStyleHeading": { 27 | "message": "Edit style" 28 | }, 29 | "license": { 30 | "message": "Licence" 31 | }, 32 | "manageFaviconsGray": { 33 | "message": "Greyed out" 34 | }, 35 | "optionsBadgeDisabled": { 36 | "message": "Background colour when disabled" 37 | }, 38 | "optionsBadgeNormal": { 39 | "message": "Background colour" 40 | }, 41 | "optionsUpdateInterval": { 42 | "message": "Userstyle auto-update interval in hours (specify 0 to disable)" 43 | }, 44 | "styleInstallFailed": { 45 | "message": "Failed to install userstyle\n$error$", 46 | "placeholders": { 47 | "error": { 48 | "content": "$1" 49 | } 50 | } 51 | }, 52 | "styleRegexpPartialExplanation": { 53 | "message": "This style uses partially matching regexps in violation of CSS4 @document specification which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome, which incorrectly checks 'regexp()' rules since the very first version (known bug)." 54 | }, 55 | "styleUpdateDiscardChanges": { 56 | "message": "The style has been changed outside the editor. Would you like to reload the style?" 57 | }, 58 | "usercssConfigIncomplete": { 59 | "message": "The style was updated or deleted after the configuration dialogue was shown. These variables were not saved to avoid corrupting the style's metadata:" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/_locales/fa/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "InaccessibleFileHint": { 3 | "message": "استایلوس نمی‌تواند به برخی از انواع فایل (مانند pdf و json) دسترسی پیدا کند." 4 | }, 5 | "addStyleLabel": { 6 | "message": "نوشتن سبک جدید" 7 | }, 8 | "addStyleTitle": { 9 | "message": "افزودن سبک" 10 | }, 11 | "alphaChannel": { 12 | "message": "شفافیت" 13 | }, 14 | "appliesAdd": { 15 | "message": "افزودن" 16 | }, 17 | "appliesDisplay": { 18 | "message": "اعمال می‌شود به: $applies$", 19 | "placeholders": { 20 | "applies": { 21 | "content": "$1" 22 | } 23 | } 24 | }, 25 | "appliesDisplayTruncatedSuffix": { 26 | "message": "و غیره" 27 | }, 28 | "appliesDomainOption": { 29 | "message": "نشانی‌ها در دامنه" 30 | }, 31 | "appliesLabel": { 32 | "message": "اعمال می‌شود" 33 | }, 34 | "appliesLineWidgetLabel": { 35 | "message": "نمایش اطلاعات 'اعمال می‌شود'" 36 | }, 37 | "appliesLineWidgetWarning": { 38 | "message": "با CSS های فشرده‌شده کار نمی‌کند" 39 | }, 40 | "appliesRegexpOption": { 41 | "message": "نشانی‌های مطابق بر عبارت باقاعده" 42 | }, 43 | "appliesRemove": { 44 | "message": "حذف" 45 | }, 46 | "appliesRemoveError": { 47 | "message": "نمی توان آخرین ورودی \"اعمال می‌شود\" را حذف کرد" 48 | }, 49 | "genericAdd": { 50 | "message": "افزودن" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/_locales/lv/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addStyleTitle": { 3 | "message": "Pievienot stilu" 4 | }, 5 | "appliesAdd": { 6 | "message": "Pievienot" 7 | }, 8 | "appliesRemove": { 9 | "message": "Noņemt" 10 | }, 11 | "author": { 12 | "message": "Autors" 13 | }, 14 | "bckpInstStyles": { 15 | "message": "Eksportēt stilus" 16 | }, 17 | "checkingForUpdate": { 18 | "message": "Pārbauda..." 19 | }, 20 | "configureStyle": { 21 | "message": "Konfigurēt" 22 | }, 23 | "confirmCancel": { 24 | "message": "Atcelt" 25 | }, 26 | "confirmClose": { 27 | "message": "Aizvērt" 28 | }, 29 | "confirmDelete": { 30 | "message": "Dzēst" 31 | }, 32 | "confirmDiscardChanges": { 33 | "message": "Atmest izmaiņas?" 34 | }, 35 | "confirmSave": { 36 | "message": "Saglabāt" 37 | }, 38 | "copied": { 39 | "message": "Kopēts starpliktuvē" 40 | }, 41 | "copy": { 42 | "message": "Kopēt starpliktuvē" 43 | }, 44 | "dateAbbrHour": { 45 | "message": "$value$s", 46 | "placeholders": { 47 | "value": { 48 | "content": "$1" 49 | } 50 | } 51 | }, 52 | "dateAbbrYear": { 53 | "message": "$value$g", 54 | "placeholders": { 55 | "value": { 56 | "content": "$1" 57 | } 58 | } 59 | }, 60 | "dateInstalled": { 61 | "message": "Uzstādīšanas datums" 62 | }, 63 | "dateUpdated": { 64 | "message": "Atjaunināšanas datums" 65 | }, 66 | "deleteStyleConfirm": { 67 | "message": "Vai esiet pārliecināts, ka vēlaties izdzēst šo stilu?" 68 | }, 69 | "deleteStyleLabel": { 70 | "message": "Dzēst" 71 | }, 72 | "disableStyleLabel": { 73 | "message": "Atspējot" 74 | }, 75 | "editDeleteText": { 76 | "message": "Dzēst" 77 | }, 78 | "editStyleLabel": { 79 | "message": "Rediģēt" 80 | }, 81 | "genericAdd": { 82 | "message": "Pievienot" 83 | }, 84 | "genericDescription": { 85 | "message": "Apraksts" 86 | }, 87 | "genericDisabledLabel": { 88 | "message": "Atspējots" 89 | }, 90 | "genericSavedMessage": { 91 | "message": "Saglabāts" 92 | }, 93 | "genericSize": { 94 | "message": "Izmērs" 95 | }, 96 | "genericTitle": { 97 | "message": "Nosaukums" 98 | }, 99 | "linterJSONError": { 100 | "message": "Nederīgs JSON formāts" 101 | }, 102 | "optionsBadgeNormal": { 103 | "message": "Fona krāsa" 104 | }, 105 | "optionsCheck": { 106 | "message": "Atjaunināt stilus" 107 | }, 108 | "optionsOpen": { 109 | "message": "Atvērt" 110 | }, 111 | "optionsSyncPassword": { 112 | "message": "Parole" 113 | }, 114 | "optionsSyncUsername": { 115 | "message": "Lietotājvārds" 116 | }, 117 | "retrieveBckp": { 118 | "message": "Importēt stilus" 119 | }, 120 | "retrieveDropboxSync": { 121 | "message": "Dropbox importēšana" 122 | }, 123 | "search": { 124 | "message": "Meklēt" 125 | }, 126 | "sortDateNewestFirst": { 127 | "message": "jaunākie vispirms" 128 | }, 129 | "sortDateOldestFirst": { 130 | "message": "vecākie vispirms" 131 | }, 132 | "styleMissingName": { 133 | "message": "Ievadiet nosaukumu" 134 | }, 135 | "styleSaveLabel": { 136 | "message": "Saglabāt" 137 | }, 138 | "unreachableAMO": { 139 | "message": "Firefox aizliedz piekļuvi tīmekļa vietnei." 140 | }, 141 | "uploadingFile": { 142 | "message": "Augšupielādē failu..." 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/_locales/te/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addStyleLabel": { 3 | "message": "క్రొత్త స్టైల్ వ్రాయండి" 4 | }, 5 | "appliesAdd": { 6 | "message": "చేర్చు" 7 | }, 8 | "appliesDisplay": { 9 | "message": "వేటికి వర్తిస్తుంది; $applies$", 10 | "placeholders": { 11 | "applies": { 12 | "content": "$1" 13 | } 14 | } 15 | }, 16 | "appliesDisplayTruncatedSuffix": { 17 | "message": "ఇంకా మరిన్ని" 18 | }, 19 | "appliesRemove": { 20 | "message": "తొలగించు" 21 | }, 22 | "appliesToEverything": { 23 | "message": "అన్నిటికీ" 24 | }, 25 | "confirmDelete": { 26 | "message": "తొలగించు" 27 | }, 28 | "confirmSave": { 29 | "message": "భద్రపరచు" 30 | }, 31 | "deleteStyleConfirm": { 32 | "message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?" 33 | }, 34 | "deleteStyleLabel": { 35 | "message": "తొలగించు" 36 | }, 37 | "disableStyleLabel": { 38 | "message": "అచేతనించు" 39 | }, 40 | "editDeleteText": { 41 | "message": "తొలగించు" 42 | }, 43 | "editStyleLabel": { 44 | "message": "మార్చు" 45 | }, 46 | "enableStyleLabel": { 47 | "message": "చేతనించు" 48 | }, 49 | "genericAdd": { 50 | "message": "చేర్చు" 51 | }, 52 | "helpAlt": { 53 | "message": "సహాయం" 54 | }, 55 | "manageHeading": { 56 | "message": "స్థాపిత శైలులు" 57 | }, 58 | "manageTitle": { 59 | "message": "స్టైలిష్" 60 | }, 61 | "sections": { 62 | "message": "విభాగాలు" 63 | }, 64 | "styleSaveLabel": { 65 | "message": "భద్రపరచు" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/background.html: -------------------------------------------------------------------------------- 1 | <%= htmlWebpackPlugin.tags.headTags %> 2 | <%= htmlWebpackPlugin.tags.bodyTags %> 3 | 4 | -------------------------------------------------------------------------------- /src/background/broadcast-injector-config.js: -------------------------------------------------------------------------------- 1 | import {pKeepAlive} from '@/js/consts'; 2 | import * as prefs from '@/js/prefs'; 3 | import {isEmptyObj} from '@/js/util'; 4 | import {broadcast} from './broadcast'; 5 | import {bgBusy, onSchemeChange} from './common'; 6 | 7 | let cfg; 8 | let sentCfg = {}; 9 | const INJECTOR_CONFIG_MAP = { 10 | exposeIframes: 'top', 11 | disableAll: 'off', 12 | [pKeepAlive]: 'wake', 13 | styleViaASS: 'ass', 14 | }; 15 | 16 | bgBusy.then(() => { 17 | prefs.subscribe(Object.keys(INJECTOR_CONFIG_MAP), broadcastInjectorConfig); 18 | }); 19 | onSchemeChange.add(broadcastInjectorConfig.bind(null, 'dark')); 20 | 21 | export default function broadcastInjectorConfig(key, val) { 22 | key = INJECTOR_CONFIG_MAP[key] || key; 23 | if (key === pKeepAlive) 24 | val = val >= 0; 25 | if (!cfg) { 26 | cfg = {}; 27 | cfg[key] = val; 28 | setTimeout(throttle); 29 | } else if (sentCfg[key] === val) { 30 | delete cfg[key]; 31 | } else { 32 | cfg[key] = val; 33 | } 34 | } 35 | 36 | const data = { 37 | method: 'injectorConfig', 38 | cfg, 39 | }; 40 | 41 | function throttle() { 42 | if (!isEmptyObj(cfg)) { 43 | data.cfg = cfg; 44 | broadcast(data); 45 | } 46 | sentCfg = cfg; 47 | cfg = null; 48 | } 49 | -------------------------------------------------------------------------------- /src/background/broadcast.js: -------------------------------------------------------------------------------- 1 | import '@/js/browser'; 2 | import {rxIgnorableError} from '@/js/msg-api'; 3 | import {ownRoot} from '@/js/urls'; 4 | import {sleep0} from '@/js/util'; 5 | import {getWindowClients} from './util'; 6 | 7 | let toBroadcast; 8 | 9 | export function broadcast(data) { 10 | toBroadcast ??= (setTimeout(doBroadcast), []); 11 | toBroadcast.push(data); 12 | } 13 | 14 | async function doBroadcast() { 15 | const [clients, tabs] = await Promise.all([ 16 | __.MV3 && getWindowClients(), // TODO: detect the popup in Chrome MV2 incognito window? 17 | browser.tabs.query({}), 18 | ]); 19 | const data = toBroadcast; 20 | toBroadcast = null; 21 | if (!__.MV3 || clients[0]) 22 | broadcastExtension(data, true); 23 | let cnt = 0; 24 | let url; 25 | tabs.sort((a, b) => b.active - a.active); // start with active tabs in all windows 26 | for (const t of tabs) { 27 | if (!t.discarded && (url = t.url) && !url.startsWith(ownRoot)) { 28 | sendTab(t.id, data, null, true); 29 | /* Broadcast messages are tiny, but sending them takes some time anyway, 30 | so we're yielding for a possible navigation/messaging event. */ 31 | if (++cnt > 50) { 32 | cnt = 0; 33 | await sleep0(); 34 | } 35 | } 36 | } 37 | } 38 | 39 | export function broadcastExtension(data, multi) { 40 | return unwrap(browser.runtime.sendMessage({data, multi})); 41 | } 42 | 43 | export function pingTab(tabId, frameId = 0) { 44 | return sendTab(tabId, {method: 'ping'}, {frameId}); 45 | } 46 | 47 | export function sendTab(tabId, data, options, multi) { 48 | return unwrap(browser.tabs.sendMessage(tabId, {data, multi}, options), multi); 49 | } 50 | 51 | async function unwrap(promise, multi) { 52 | const err = new Error(); 53 | let data, error; 54 | try { 55 | ({data, error} = await promise || {}); 56 | if (!error) return data; 57 | } catch (e) { 58 | error = e; 59 | if (rxIgnorableError.test(err.message = e.message)) { 60 | return; 61 | } 62 | } 63 | if (error.stack) 64 | err.stack = error.stack + '\n' + err.stack; 65 | if (multi) { 66 | console.error(err); 67 | return data; 68 | } 69 | return Promise.reject(err); 70 | } 71 | -------------------------------------------------------------------------------- /src/background/browser-cmd-hotkeys.js: -------------------------------------------------------------------------------- 1 | import '@/js/browser'; 2 | import {knownKeys, subscribe} from '@/js/prefs'; 3 | 4 | export default function initBrowserCommandsApi() { 5 | const browserCommands = browser.commands; 6 | if (!browserCommands?.update) return; 7 | subscribe(knownKeys.filter(k => k.startsWith('hotkey.')), async (name, value) => { 8 | try { 9 | if (value.trim()) { 10 | await browserCommands.update({ 11 | name: name.split('.')[1], 12 | shortcut: value, 13 | }); 14 | } 15 | } catch {} 16 | }, true); 17 | } 18 | -------------------------------------------------------------------------------- /src/background/common.js: -------------------------------------------------------------------------------- 1 | import {k_busy, kResolve} from '@/js/consts'; 2 | import {CHROME} from '@/js/ua'; 3 | import {browserWindows} from '@/js/util-webext'; 4 | 5 | /** Minimal init for a wake-up event */ 6 | export const bgPreInit = []; 7 | export const bgInit = []; 8 | /** @type {Map} */ 9 | export const clientDataJobs = __.MV3 && new Map(); 10 | 11 | /** Temporary storage for data needed elsewhere e.g. in a content script */ 12 | export const dataHub = { 13 | del: key => delete data[key], 14 | get: key => data[key], 15 | has: key => key in data, 16 | pop: key => { 17 | const val = data[key]; 18 | delete data[key]; 19 | return val; 20 | }, 21 | set: (key, val) => { 22 | data[key] = val; 23 | }, 24 | }; 25 | const data = {__proto__: null}; 26 | 27 | /** @type {Set<(isDark: boolean) => ?>} */ 28 | export const onSchemeChange = new Set(); 29 | /** @type {Set<(tabId: number, url: string, oldUrl?: string) => ?>} */ 30 | export const onTabUrlChange = new Set(); 31 | /** @type {Set<(tabId: number, frameId: number, port: chrome.runtime.Port) => ?>} */ 32 | export const onUnload = new Set(); 33 | /** @type {Set<(data: Object, type: 'committed'|'history'|'hash') => ?>} */ 34 | export const onUrlChange = new Set(); 35 | 36 | export const uuidIndex = Object.assign(new Map(), { 37 | custom: {}, 38 | /** `obj` must have a unique `id`, a UUIDv4 `_id`, and Date.now() for `_rev`. */ 39 | addCustom(obj, {get = () => obj, set}) { 40 | Object.defineProperty(uuidIndex.custom, obj._id, {get, set}); 41 | }, 42 | }); 43 | 44 | export let isVivaldi = !!(browserWindows && CHROME) && (async () => { 45 | const wnd = (await browserWindows.getAll())[0] || 46 | await new Promise(resolve => browserWindows.onCreated.addListener(function onCreated(w) { 47 | browserWindows.onCreated.removeListener(onCreated); 48 | resolve(w); 49 | })); 50 | isVivaldi = !!(wnd && (wnd.vivExtData || wnd.extData)); 51 | return isVivaldi; 52 | })(); 53 | 54 | export let bgBusy = global[k_busy] = (_ => 55 | Object.assign(new Promise(cb => (_ = cb)), {[kResolve]: _}) 56 | )(); 57 | 58 | bgBusy.then(() => { 59 | bgBusy = null; 60 | delete global[k_busy]; 61 | }); 62 | 63 | if (__.DEBUG) { 64 | global._bgPreInit = bgPreInit; 65 | global._bgInit = bgInit; 66 | bgPreInit.push = (...args) => { 67 | const {stack} = new Error(); 68 | for (const a of args) if (a && typeof a === 'object') a._stack = stack; 69 | return [].push.apply(bgPreInit, args); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/background/db-chrome-storage.js: -------------------------------------------------------------------------------- 1 | import {DB, UCD} from '@/js/consts'; 2 | import {chromeLocal, GET_KEYS} from '@/js/storage-util'; 3 | 4 | export default class ChromeStorageDB { 5 | 6 | constructor(dbName, mirror) { 7 | this._max = dbName === DB ? 0 : 1; 8 | this._mirror = mirror; 9 | this._prefix = dbName === DB ? 'style-' : `${dbName}-`; 10 | } 11 | 12 | delete(id) { 13 | return chromeLocal.remove(this._prefix + id); 14 | } 15 | 16 | async get(id) { 17 | return (await chromeLocal.get(id = this._prefix + id))[id]; 18 | } 19 | 20 | async getAll() { 21 | const all = !GET_KEYS && await chromeLocal.get(); 22 | const keys = GET_KEYS ? await chromeLocal.getKeys() : Object.keys(all); 23 | const res = []; 24 | if (!this._max) 25 | await this._init(keys); 26 | for (const key of keys) 27 | if (key.startsWith(this._prefix)) 28 | res.push(GET_KEYS ? key : all[key]); 29 | return GET_KEYS 30 | ? Object.values(await chromeLocal.get(res)) 31 | : res; 32 | } 33 | 34 | async put(item, key) { 35 | key ??= item.id ??= (!this._max && await this._init(), this._max++); 36 | await chromeLocal.set({ 37 | [this._prefix + key]: this._mirror && item[UCD] 38 | ? {...item, sections: undefined} 39 | : item, 40 | }); 41 | return key; 42 | } 43 | 44 | async putMany(items, keys) { 45 | const data = {}; 46 | const res = []; 47 | for (let i = 0; i < items.length; i++) { 48 | const item = items[i]; 49 | const id = keys ? keys[i] : item.id ??= (!this._max && await this._init(), this._max++); 50 | data[this._prefix + id] = this._mirror && item[UCD] 51 | ? {...item, sections: undefined} 52 | : item; 53 | res.push(id); 54 | } 55 | await chromeLocal.set(data); 56 | return res; 57 | } 58 | 59 | async _init(keys) { 60 | let res = 1; 61 | let id; 62 | keys ??= GET_KEYS 63 | ? await chromeLocal.getKeys() 64 | : Object.keys(await chromeLocal.get()); 65 | for (const key of keys) 66 | if (key.startsWith(this._prefix) && (id = +key.slice(this._prefix.length)) >= res) 67 | res = id + 1; 68 | this._max = res; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/background/db-to-cloud-broker.js: -------------------------------------------------------------------------------- 1 | import dropbox from 'db-to-cloud/lib/drive/dropbox'; 2 | import onedrive from 'db-to-cloud/lib/drive/onedrive'; 3 | import google from 'db-to-cloud/lib/drive/google'; 4 | import webdav from 'db-to-cloud/lib/drive/webdav'; 5 | 6 | export const cloudDrive = {dropbox, onedrive, google, webdav: !__.MV3 && webdav}; 7 | export {dbToCloud} from 'db-to-cloud/lib/db-to-cloud'; 8 | -------------------------------------------------------------------------------- /src/background/intro.js: -------------------------------------------------------------------------------- 1 | global[__.API] = {}; // `global.API` will be used by msg.js 2 | if (!__.MV3) global._bg = true; // for IS_BG check 3 | -------------------------------------------------------------------------------- /src/background/offscreen.js: -------------------------------------------------------------------------------- 1 | import {createPortProxy} from '@/js/port'; 2 | import {ownRoot} from '@/js/urls'; 3 | import {getWindowClients} from './util'; 4 | 5 | const FILENAME = 'offscreen.html'; 6 | const DOC_URL = ownRoot + FILENAME; 7 | 8 | /** @type {OffscreenAPI | CommandsAPI} */ 9 | const offscreen = createPortProxy(() => ( 10 | creating ??= create().finally(done) 11 | ), { 12 | lock: '/' + FILENAME, 13 | }); 14 | export default offscreen; 15 | 16 | let creating; 17 | 18 | async function findOffscreenClient() { 19 | for (const c of await getWindowClients()) 20 | if (c.url === DOC_URL) 21 | return c; 22 | } 23 | 24 | async function create() { 25 | __.DEBUGTRACE('getDoc creating...'); 26 | try { 27 | await chrome.offscreen.createDocument({ 28 | url: DOC_URL, 29 | reasons: ['BLOBS', 'DOM_PARSER', 'MATCH_MEDIA', 'WORKERS'], 30 | justification: 'ManifestV3 requirement', 31 | }); 32 | } catch (err) { 33 | if (!err.message.startsWith('Only a single offscreen')) throw err; 34 | } 35 | __.DEBUGLOG('getDoc created'); 36 | return findOffscreenClient(); 37 | } 38 | 39 | function done() { 40 | creating = null; 41 | __.DEBUGLOG('getDoc done'); 42 | } 43 | -------------------------------------------------------------------------------- /src/background/prefs-api.js: -------------------------------------------------------------------------------- 1 | import * as chromeSync from '@/js/chrome-sync'; 2 | import * as prefs from '@/js/prefs'; 3 | import {debounce, deepEqual, deepMerge, isObject} from '@/js/util'; 4 | import {bgBusy, bgPreInit} from './common'; 5 | 6 | /** Only the non-default preferences. 7 | * WARNING for bg context: properties of object type are direct references into `values`! 8 | * In non-bg contexts this is correctly deep-copied by msg.js::API. */ 9 | export const nondefaults = {}; 10 | export const setPrefs = data => { 11 | for (const k in data) 12 | prefs.set(k, data[k]); 13 | }; 14 | 15 | const updateStorage = () => chromeSync.set({[prefs.STORAGE_KEY]: nondefaults}); 16 | 17 | prefs.set._bgSet = (key, val) => { 18 | const def = prefs.__defaults[key]; 19 | if (val !== def && !(val && typeof def === 'object' && deepEqual(val, def))) { 20 | nondefaults[key] = val; 21 | } else if (key in nondefaults) { 22 | delete nondefaults[key]; 23 | } else { 24 | return; 25 | } 26 | if (!bgBusy) debounce(updateStorage); 27 | return true; 28 | }; 29 | 30 | bgPreInit.push(chromeSync.get(prefs.STORAGE_KEY).then(orig => { 31 | orig = orig[prefs.STORAGE_KEY]; 32 | __.DEBUGLOG('prefsApi', orig); 33 | prefs.ready.set(isObject(orig) ? deepMerge(orig) : {}, {}); 34 | if (!deepEqual(orig, nondefaults)) bgBusy.then(updateStorage); 35 | return prefs.ready; 36 | })); 37 | -------------------------------------------------------------------------------- /src/background/set-client-data.js: -------------------------------------------------------------------------------- 1 | import {kPopup, UCD} from '@/js/consts'; 2 | import {API} from '@/js/msg-api'; 3 | import * as prefs from '@/js/prefs'; 4 | import {FIREFOX} from '@/js/ua'; 5 | import {isDark, setSystemDark} from './color-scheme'; 6 | import {bgBusy, dataHub, isVivaldi} from './common'; 7 | import {prefsDB, stateDB} from './db'; 8 | import makePopupData from './popup-data'; 9 | import {nondefaults} from './prefs-api'; 10 | import * as styleMan from './style-manager'; 11 | import {webRequestBlocking} from './style-via-webrequest'; 12 | import * as syncMan from './sync-manager'; 13 | import * as usercssTemplate from './usercss-template'; 14 | 15 | const kEditorScrollInfo = 'editorScrollInfo'; 16 | /** @type {ResponseInit} */ 17 | const RESPONSE_INIT = { 18 | headers: {'cache-control': 'no-cache'}, 19 | }; 20 | const ASSIGN_FUNC_STR = __.MV3 && `${function (data) { 21 | Object.assign(this[__.CLIENT_DATA], data); 22 | }}`; 23 | const PROVIDERS = { 24 | edit(url) { 25 | const id = +url.searchParams.get('id'); 26 | const style = styleMan.get(id); 27 | const isUC = style ? UCD in style : prefs.__values.newStyleAsUsercss; 28 | const siKey = kEditorScrollInfo + id; 29 | return /** @namespace StylusClientData */ { 30 | style, 31 | isUC, 32 | si: style && (__.MV3 ? stateDB.get(siKey) : dataHub[siKey]), 33 | template: !style && isUC && (usercssTemplate.value || usercssTemplate.load()), 34 | }; 35 | }, 36 | manage(url) { 37 | const sp = url.searchParams; 38 | const query = sp.get('search') || undefined/*to enable client's parameter default value*/; 39 | return /** @namespace StylusClientData */ { 40 | badFavs: prefs.__values['manage.newUI'] 41 | && prefs.__values['manage.newUI.favicons'] 42 | && prefsDB.get('badFavs'), 43 | ids: query 44 | && styleMan.searchDb({ 45 | query, 46 | mode: sp.get('searchMode') || prefs.__values['manage.searchMode'], 47 | }), 48 | styles: styleMan.getCore({sections: true, size: true}), 49 | sync: syncMan.getStatus(true), 50 | }; 51 | }, 52 | options: () => { 53 | const status = syncMan.getStatus(); 54 | const {drive} = status; 55 | return /** @namespace StylusClientData */ { 56 | sync: status, 57 | syncOpts: drive ? syncMan.getDriveOptions(drive) : {}, 58 | wrb: webRequestBlocking, 59 | }; 60 | }, 61 | popup: () => ({ 62 | [kPopup]: dataHub.pop(kPopup) || makePopupData(), 63 | }), 64 | }; 65 | 66 | /** @namespace API */ 67 | Object.assign(API, { 68 | saveScroll(id, info) { 69 | if (__.MV3) stateDB.put(info, kEditorScrollInfo + id); 70 | else dataHub[kEditorScrollInfo + id] = info; 71 | }, 72 | }); 73 | 74 | export default async function setClientData({ 75 | dark: pageDark, 76 | url: pageUrl, 77 | } = {}) { 78 | setSystemDark(pageDark); 79 | if (bgBusy) await bgBusy; 80 | const url = new URL(pageUrl); 81 | const page = url.pathname.slice(1/*"/"*/, -5/*".html"*/); 82 | const jobs = /** @namespace StylusClientData */ Object.assign({ 83 | apply: styleMan.getSectionsByUrl(pageUrl, {init: true}), 84 | dark: isDark, 85 | favicon: FIREFOX || isVivaldi, 86 | prefs: nondefaults, 87 | }, PROVIDERS[page]?.(url)); 88 | const results = await Promise.all(Object.values(jobs)); 89 | Object.keys(jobs).forEach((id, i) => (jobs[id] = results[i])); 90 | return __.MV3 91 | ? new Response(`(${ASSIGN_FUNC_STR})(${JSON.stringify(jobs)})`, RESPONSE_INIT) 92 | : jobs; 93 | } 94 | -------------------------------------------------------------------------------- /src/background/style-manager/cache-builder.js: -------------------------------------------------------------------------------- 1 | import {styleCodeEmpty} from '@/js/sections-util'; 2 | import cacheData, * as styleCache from './cache'; 3 | import {urlMatchSection, urlMatchStyle} from './matcher'; 4 | import {dataMap} from './util'; 5 | 6 | /** @param {StyleObj} style 7 | * @return {void} */ 8 | export function buildCacheForStyle(style) { 9 | const {id} = style; 10 | const data = dataMap.get(id); 11 | // FIXME: ideally, when preview is available, there is no need to rebuild the cache when original style change. 12 | // we should lift this logic to parent function. 13 | const styleToApply = data.preview || style; 14 | const updated = new Set(); 15 | for (const cache of cacheData.values()) { 16 | const url = cache.url; 17 | if (!data.appliesTo.has(url)) { 18 | (cache.maybeMatch ??= new Set()).add(id); 19 | continue; 20 | } 21 | const code = getAppliedCode({url}, styleToApply); 22 | if (code) { 23 | updated.add(url); 24 | buildCacheEntry(cache, styleToApply, code); 25 | } else if (cache.sections[id]) { 26 | delete cache.sections[id]; 27 | } else { 28 | continue; 29 | } 30 | styleCache.hit(cache); 31 | } 32 | data.appliesTo = updated; 33 | } 34 | 35 | /** 36 | * @param {MatchCache.Entry} cache 37 | * @param {string} url 38 | * @param {Iterable} [ids] 39 | * @return {void} */ 40 | export function buildCache(cache, url, ids) { 41 | const query = {url}; 42 | for (let data of ids || dataMap.values()) { 43 | if (ids && !(data = dataMap.get(data))) 44 | continue; 45 | const {style} = data; 46 | // getSectionsByUrl only needs enabled styles 47 | const code = style.enabled && getAppliedCode(query, data.preview || style); 48 | if (code) { 49 | buildCacheEntry(cache, style, code); 50 | data.appliesTo.add(url); 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * @param {MatchCache.Entry} entry 57 | * @param {StyleObj} style 58 | * @param {MatchCache.Index} [idx] 59 | * @param {string[]} [code] 60 | */ 61 | function buildCacheEntry(entry, style, [idx, code]) { 62 | styleCache.make(entry, style, idx, code); 63 | } 64 | 65 | /** Get styles matching a URL, including sloppy regexps and excluded items. 66 | * @param {MatchQuery} query 67 | * @param {StyleObj} style 68 | * @return {?Array} 69 | */ 70 | function getAppliedCode(query, style) { 71 | const result = urlMatchStyle(query, style); 72 | const isIncluded = result === 'included'; 73 | const code = []; 74 | const idx = []; 75 | if (!isIncluded && result !== true) { 76 | return; 77 | } 78 | let i = 0; 79 | for (const section of style.sections) { 80 | if ((isIncluded || urlMatchSection(query, section) === true) 81 | && !styleCodeEmpty(section)) { 82 | code.push(section.code); 83 | idx.push(i); 84 | } 85 | i++; 86 | } 87 | return code.length && [idx, code]; 88 | } 89 | -------------------------------------------------------------------------------- /src/background/style-manager/cache.js: -------------------------------------------------------------------------------- 1 | import {bgBusy} from '../common'; 2 | 3 | let onDeleted; 4 | let timer; 5 | const MAX = 1000; 6 | /** @type {Map} keyed on URL */ 7 | const cache = new Map(); 8 | /** @type {Set} */ 9 | const toWrite = new Set(); 10 | 11 | export default cache; 12 | 13 | /** @param {MatchCache.Entry} val 14 | * @return {void} */ 15 | export function add(val) { 16 | cache.set(val.url, hit(val)); 17 | if (cache.size >= MAX) prune(); 18 | } 19 | 20 | /** @return {void} */ 21 | export function clear() { 22 | if (onDeleted) cache.forEach(onDeleted); 23 | if (timer) timer = clearTimeout(timer); 24 | cache.clear(); 25 | } 26 | 27 | /** 28 | * @param {MatchCache.DbEntry} entry 29 | * @param {StyleObj} style 30 | * @param {MatchCache.Index} [idx] 31 | * @param {string[]} [code] 32 | * @return {?boolean} 33 | */ 34 | export function make(entry, style, idx, code) { 35 | const id = style.id; 36 | const entrySections = entry.sections; 37 | if (idx || (idx = entrySections[id]) && !idx.idx) { 38 | if (!code) { 39 | code = []; 40 | for (const i of idx) { 41 | const sec = style.sections[i]; 42 | if (sec) code.push(sec.code); 43 | else return; 44 | } 45 | } 46 | entrySections[id] = { 47 | id, 48 | idx, 49 | code, 50 | name: style.customName || style.name, 51 | }; 52 | } 53 | return !!idx; 54 | } 55 | 56 | export function setOnDeleted(fn) { 57 | onDeleted = fn; 58 | } 59 | 60 | /** @sideeffects Overwrites the array 61 | * @param {MatchCache.Entry} 62 | * @return {void} */ 63 | function del(items) { 64 | if (!items[0]) return; 65 | for (let i = 0, val; i < items.length; i++) { 66 | val = items[i]; 67 | cache.delete(items[i] = val.url); 68 | onDeleted(val); 69 | } 70 | } 71 | 72 | /** @return {void} */ 73 | function flush() { 74 | for (const val of toWrite) 75 | val.d = [val.d?.[1] || 0, new Date()]; 76 | toWrite.clear(); 77 | timer = null; 78 | } 79 | 80 | /** @return {Promise} */ 81 | async function flushLater() { 82 | timer = setTimeout(flush, bgBusy 83 | ? (await bgBusy, 5000) // to let the browser settle down on startup 84 | : 50); 85 | } 86 | 87 | /** @template {MatchCache.Entry} T 88 | * @param {T} val 89 | * @return {T} */ 90 | export function hit(val) { 91 | if (val) { 92 | toWrite.add(val); 93 | if (!timer) flushLater(); 94 | } 95 | return val; 96 | } 97 | 98 | /** @return {void} */ 99 | function prune() { 100 | del([...cache.values()] 101 | .filter(val => val.d) 102 | .sort(({d: [a1, a2]}, {d: [b1, b2]}) => 103 | 100 * (a1 - b1) + 104 | 10 * ((b2 - b1) - (a2 - a1)) + 105 | a2 - b2) 106 | .slice(0, 10)); 107 | } 108 | -------------------------------------------------------------------------------- /src/background/style-manager/init.js: -------------------------------------------------------------------------------- 1 | import {DB, kInjectionOrder, kResolve} from '@/js/consts'; 2 | import {onConnect, onDisconnect} from '@/js/msg'; 3 | import {STORAGE_KEY} from '@/js/prefs'; 4 | import * as colorScheme from '../color-scheme'; 5 | import {bgBusy, bgInit, onSchemeChange} from '../common'; 6 | import {db, draftsDB, execMirror, prefsDB} from '../db'; 7 | import * as styleCache from './cache'; 8 | import './init'; 9 | import {fixKnownProblems} from './fixer'; 10 | import {broadcastStyleUpdated, dataMap, setOrderImpl, storeInMap} from './util'; 11 | 12 | bgInit.push(async () => { 13 | __.DEBUGLOG('styleMan init...'); 14 | let mirrored; 15 | let [orderFromDb, styles] = await Promise.all([ 16 | prefsDB.get(kInjectionOrder), 17 | db.getAll(), 18 | ]); 19 | if (!orderFromDb) 20 | orderFromDb = await execMirror(STORAGE_KEY, 'get', kInjectionOrder); 21 | if (!styles[0]) 22 | styles = mirrored = await execMirror(DB, 'getAll'); 23 | setOrderImpl(orderFromDb, {store: false}); 24 | initStyleMap(styles, mirrored); 25 | __.DEBUGLOG('styleMan init done'); 26 | }); 27 | 28 | onSchemeChange.add(() => { 29 | for (const {style} of dataMap.values()) { 30 | if (colorScheme.SCHEMES.includes(style.preferScheme)) { 31 | broadcastStyleUpdated(style, 'colorScheme'); 32 | } 33 | } 34 | }); 35 | 36 | styleCache.setOnDeleted(val => { 37 | for (const id in val.sections) { 38 | dataMap.get(+id)?.appliesTo.delete(val.url); 39 | } 40 | }); 41 | 42 | // Using ports to reliably track when the client is closed, however not for messaging, 43 | // because our `API` is much faster due to direct invocation. 44 | onDisconnect.draft = port => { 45 | if (__.MV3) port[kResolve](); 46 | const id = port.name.split(':')[1]; 47 | draftsDB.delete(+id || id).catch(() => { 48 | }); 49 | }; 50 | 51 | onDisconnect.livePreview = port => { 52 | if (__.MV3) port[kResolve](); 53 | const id = +port.name.split(':')[1]; 54 | const data = dataMap.get(id); 55 | if (!data) return; 56 | data.preview = null; 57 | broadcastStyleUpdated(data.style, 'editPreviewEnd'); 58 | }; 59 | 60 | if (__.MV3) { 61 | onConnect.draft = onConnect.livePreview = port => { 62 | __.KEEP_ALIVE(new Promise(resolve => { 63 | port[kResolve] = resolve; 64 | })); 65 | }; 66 | } 67 | 68 | async function initStyleMap(styles, mirrored) { 69 | let fix, fixed, lost, i, style, len; 70 | for (i = 0, len = 0, style; i < styles.length; i++) { 71 | style = styles[i]; 72 | if (+style.id > 0 73 | && typeof style._id === 'string' 74 | && typeof style.sections?.[0]?.code === 'string') { 75 | storeInMap(style); 76 | if (mirrored) { 77 | if (i > len) styles[len] = style; 78 | len++; 79 | } 80 | } else { 81 | try { fix = fixKnownProblems(style, true); } catch {} 82 | if (fix) (fixed ??= new Map()).set(style.id, fix); 83 | else (lost ??= []).push(style); 84 | } 85 | } 86 | styles.length = len; 87 | if (lost) 88 | console.error(`Skipped ${lost.length} unrecoverable styles:`, lost); 89 | if (fixed) { 90 | console[mirrored ? 'log' : 'warn'](`Fixed ${fixed.size} styles, ids:`, ...fixed.keys()); 91 | fixed = await Promise.all([...fixed.values(), bgBusy]); 92 | fixed.pop(); 93 | if (mirrored) { 94 | styles.push(...fixed); 95 | fixed.forEach(storeInMap); 96 | } 97 | } 98 | if (styles.length) 99 | setTimeout(db.putMany, 100, styles); 100 | } 101 | -------------------------------------------------------------------------------- /src/background/style-manager/util.js: -------------------------------------------------------------------------------- 1 | import {kInjectionOrder, UCD} from '@/js/consts'; 2 | import * as URLS from '@/js/urls'; 3 | import {deepEqual, mapObj} from '@/js/util'; 4 | import {broadcast} from '../broadcast'; 5 | import broadcastInjectorConfig from '../broadcast-injector-config'; 6 | import {uuidIndex} from '../common'; 7 | import {prefsDB} from '../db'; 8 | import * as syncMan from '../sync-manager'; 9 | import {buildCacheForStyle} from './cache-builder'; 10 | 11 | /** @type {StyleDataMap} */ 12 | export const dataMap = new Map(); 13 | 14 | export const order = /** @type {Injection.Order} */{main: {}, prio: {}}; 15 | export const orderWrap = { 16 | id: kInjectionOrder, 17 | value: mapObj(order, () => []), 18 | _id: `${chrome.runtime.id}-${kInjectionOrder}`, 19 | _rev: 0, 20 | }; 21 | 22 | export function calcRemoteId({md5Url, updateUrl, [UCD]: ucd} = {}) { 23 | let id; 24 | id = (id = /\d+/.test(md5Url) || URLS.extractUsoaId(updateUrl)) && `uso-${id}` 25 | || (id = URLS.extractUswId(updateUrl)) && `usw-${id}` 26 | || ''; 27 | return id && [ 28 | id, 29 | !!ucd?.vars, 30 | ]; 31 | } 32 | 33 | /** @returns {StyleObj} */ 34 | const createNewStyle = () => ({ 35 | enabled: true, 36 | installDate: Date.now(), 37 | }); 38 | 39 | /** @returns {StyleObj|void} */ 40 | export const getById = id => dataMap.get(+id)?.style; 41 | 42 | /** @returns {StyleObj|void} */ 43 | export const getByUuid = uuid => getById(uuidIndex.get(uuid)); 44 | 45 | /** @returns {StyleObj} */ 46 | export const mergeWithMapped = style => ({ 47 | ...getById(style.id) || createNewStyle(), 48 | ...style, 49 | }); 50 | 51 | export function broadcastStyleUpdated(style, reason, isNew) { 52 | buildCacheForStyle(style); 53 | return broadcast({ 54 | method: isNew ? 'styleAdded' : 'styleUpdated', 55 | reason, 56 | style: { 57 | id: style.id, 58 | enabled: style.enabled, 59 | }, 60 | }); 61 | } 62 | 63 | export async function setOrderImpl(data, { 64 | broadcast: broadcastAllowed, 65 | calc = true, 66 | store = true, 67 | sync, 68 | } = {}) { 69 | if (!data || !data.value || deepEqual(data.value, orderWrap.value)) { 70 | return; 71 | } 72 | Object.assign(orderWrap, data, sync && {_rev: Date.now()}); 73 | if (calc) { 74 | for (const [type, group] of Object.entries(data.value)) { 75 | const dst = order[type] = {}; 76 | group.forEach((uuid, i) => { 77 | const id = uuidIndex.get(uuid); 78 | if (id) dst[id] = i; 79 | }); 80 | } 81 | } 82 | if (broadcastAllowed) { 83 | broadcastInjectorConfig('order', order); 84 | } 85 | if (store) { 86 | await prefsDB.put(orderWrap, orderWrap.id); 87 | } 88 | if (sync) { 89 | syncMan.putDoc(orderWrap); 90 | } 91 | } 92 | 93 | /** @returns {void} */ 94 | export function storeInMap(style) { 95 | dataMap.set(style.id, { 96 | style, 97 | appliesTo: new Set(), 98 | }); 99 | uuidIndex.set(style._id, style.id); 100 | } 101 | 102 | uuidIndex.addCustom(orderWrap, {set: setOrderImpl}); 103 | -------------------------------------------------------------------------------- /src/background/sw/index.js: -------------------------------------------------------------------------------- 1 | import '../intro'; // sets global.API 2 | import './keep-alive'; // sets global.keepAlive 3 | import {kMainFrame, kSubFrame} from '@/js/consts'; 4 | import {_execute} from '@/js/msg'; 5 | import {initRemotePort} from '@/js/port'; 6 | import {ownRoot} from '@/js/urls'; 7 | import {clientDataJobs} from '../common'; 8 | import {cloudDrive} from '../db-to-cloud-broker'; 9 | import setClientData from '../set-client-data'; 10 | import offscreen from '../offscreen'; 11 | import '..'; 12 | 13 | if (__.DEBUG) { 14 | global.onunhandledrejection = global.onerror = e => { 15 | e = e.reason || e.error || e; 16 | chrome.tabs.create({url: 'data:,' + (e.stack || e.message || `${e}`)}); 17 | }; 18 | } 19 | 20 | /** @param {ExtendableEvent} evt */ 21 | global.oninstall = evt => { 22 | evt.addRoutes({ 23 | condition: {urlPattern: `${ownRoot}*.html?clientData*`}, 24 | source: 'fetch-event', 25 | }); 26 | evt.addRoutes({ 27 | condition: {not: {urlPattern: `${ownRoot}*.user.css`, requestDestination: 'document'}}, 28 | source: 'network', 29 | }); 30 | }; 31 | 32 | /** @param {FetchEvent} evt */ 33 | global.onfetch = evt => { 34 | __.DEBUGLOG('onfetch', evt.request, evt); 35 | const url = evt.request.url; 36 | if (!url.startsWith(ownRoot)) { 37 | return; // shouldn't happen but addRoutes may be bugged 38 | } 39 | if (url.includes('?clientData')) { 40 | const sp = new URL(url).searchParams; 41 | const dark = !!+sp.get('dark'); 42 | const pageUrl = sp.get('url'); 43 | const job = setClientData({dark, url: pageUrl}); 44 | clientDataJobs.set(pageUrl, job); 45 | job.finally(() => clientDataJobs.delete(pageUrl)); 46 | evt.respondWith(job); 47 | } else if (/\.user.css#(\d+)$/.test(url)) { 48 | evt.respondWith(Response.redirect('edit.html?id=' + RegExp.$1)); 49 | } 50 | }; 51 | 52 | // API 53 | global.onmessage = initRemotePort.bind(_execute); 54 | 55 | cloudDrive.webdav = async cfg => { 56 | const res = await offscreen.webdavInit(cfg); 57 | const webdav = offscreen.webdav; 58 | for (const k in res) res[k] ??= webdav.bind(null, k); 59 | return res; 60 | }; 61 | 62 | /** 63 | * This ensures that SW starts even before our page makes a clientData request inside. 64 | * The actual listener is usually invoked after `onfetch`, but there's no guarantee. 65 | */ 66 | chrome.webRequest.onBeforeRequest.addListener(() => {}, { 67 | urls: [ownRoot + '*.html*'], 68 | types: [kMainFrame, kSubFrame], 69 | }); 70 | -------------------------------------------------------------------------------- /src/background/sw/keep-alive.js: -------------------------------------------------------------------------------- 1 | import {pKeepAlive} from '@/js/consts'; 2 | import * as prefs from '@/js/prefs'; 3 | import {bgBusy} from '../common'; 4 | 5 | /** @type {?Promise[]} */ 6 | let busy; 7 | let lastBusyTime = 0; 8 | let pulse; 9 | /** ms */ 10 | let TTL; 11 | /** seconds */ 12 | let idleDuration; 13 | 14 | keepAlive(bgBusy); 15 | __.KEEP_ALIVE = keepAlive; 16 | prefs.subscribe(pKeepAlive, (_, val) => { 17 | idleDuration = Math.max(30, val * 60 | 0/*to integer*/ || 0/*if val is not a number*/); 18 | TTL = val * 60e3; 19 | if (!pulse || !TTL && !busy) reschedule(); 20 | }, true); 21 | 22 | function keepAlive(job) { 23 | if (__.DEBUG & 4) console.trace('%ckeepAlive', 'font-weight:bold', job); 24 | if (!(job instanceof Promise)) lastBusyTime = performance.now(); 25 | else if (!busy) keepAliveUntilSettled([job]); 26 | else busy.push(job); 27 | return job; 28 | } 29 | 30 | async function keepAliveUntilSettled(promises) { 31 | busy = promises; 32 | if (TTL == null && bgBusy) await bgBusy; 33 | if (!pulse) reschedule(); 34 | do await Promise.allSettled(busy); 35 | while (busy?.splice(0, promises.length) && busy.length); 36 | busy = null; 37 | lastBusyTime = performance.now(); 38 | if (__.DEBUG & 4) console.log('%ckeepAlive settled', 'font-weight:bold'); 39 | } 40 | 41 | /** 42 | * Calling an async `chrome` API keeps the SW alive for the next 30 seconds: 43 | * 1. when `busy` contains unsettled Promises, 44 | * 2. when the user explicitly wants to keep the SW alive forever (TTL < 0), 45 | * 3. when the browser is actively used and the user's TTL > 0. 46 | * Otherwise (TTL = 0), we don't call it and rely on the vanilla MV3 behavior. 47 | */ 48 | async function reschedule() { 49 | if (busy || TTL < 0 50 | ? isUserActiveInBrowser(true) // not awaiting as we don't need the result 51 | : TTL && performance.now() < lastBusyTime + TTL 52 | && await isUserActiveInBrowser(prefs.__values.keepAliveIdle)) { 53 | if (__.DEBUG & 4) console.log('keepAlive setInterval', pulse || 'set'); 54 | pulse ??= setInterval(reschedule, 25e3); 55 | } else if (pulse) { 56 | if (__.DEBUG & 4) console.log('keepAlive setInterval cleared'); 57 | clearInterval(pulse); 58 | pulse = null; 59 | } 60 | } 61 | 62 | async function isUserActiveInBrowser(yes) { 63 | return (await chrome.idle.queryState(idleDuration) !== 'idle' || yes) && 64 | (yes || (await chrome.windows.getAll({})).some(wnd => wnd.focused)); 65 | } 66 | -------------------------------------------------------------------------------- /src/background/usercss-template.js: -------------------------------------------------------------------------------- 1 | import {getLZValue, LZ_KEY, unLZ} from '@/js/chrome-sync'; 2 | import {onStorageChanged} from '@/js/util-webext'; 3 | 4 | export let value; 5 | 6 | const key = LZ_KEY.usercssTemplate; 7 | 8 | export async function load() { 9 | value ??= getLZValue(key); 10 | if (value.then) value = await value; 11 | return value; 12 | } 13 | 14 | onStorageChanged.addListener(changes => { 15 | if ((changes = changes[key])) { 16 | value = unLZ(changes.newValue); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/background/util.js: -------------------------------------------------------------------------------- 1 | import {CLIENT, createPortProxy} from '@/js/port'; 2 | import {workerPath} from '@/js/urls'; 3 | import {clientDataJobs} from './common'; 4 | import offscreen from './offscreen'; 5 | 6 | /** @return {WindowClient[]} */ 7 | export const getWindowClients = () => self.clients.matchAll({ 8 | includeUncontrolled: true, 9 | type: 'window', 10 | }); 11 | 12 | const getWorkerPortFromClient = async () => { 13 | let proxy; 14 | __.DEBUGPORT('sw -> worker -> offscreen client', offscreen[CLIENT]); 15 | if (!offscreen[CLIENT]) { 16 | for (const client of await getWindowClients()) { 17 | if (!clientDataJobs.has(client.url)) { 18 | __.DEBUGPORT('sw -> worker -> client', client); 19 | proxy = createPortProxy(client, {once: true}); 20 | break; 21 | } 22 | } 23 | } 24 | return (proxy || offscreen).getWorkerPort(workerPath); 25 | }; 26 | 27 | /** @type {WorkerAPI} */ 28 | export const worker = __.MV3 29 | ? createPortProxy(getWorkerPortFromClient, {lock: workerPath}) 30 | : createPortProxy(workerPath); 31 | -------------------------------------------------------------------------------- /src/cm/jsonlint-bundle.js: -------------------------------------------------------------------------------- 1 | import {CodeMirror} from '.'; 2 | import 'codemirror/mode/javascript/javascript'; 3 | import {parseJSON} from 'usercss-meta/lib/parse-util'; 4 | 5 | CodeMirror.registerHelper('lint', 'json', text => { 6 | let res, line, ch, i; 7 | try { 8 | parseJSON({text, lastIndex: 0}); 9 | } catch (e) { 10 | ch = 0; 11 | line = i = -1; 12 | do line++; while ((i = text.indexOf('\n', ch = i + 1)) >= 0 && i < e.index); 13 | ch = e.index - ch; 14 | res = [{ 15 | from: {line, ch}, 16 | to: {line, ch: ch + 1}, 17 | message: e.message.replace('Invalid JSON: ', ''), 18 | }]; 19 | } 20 | return res || []; 21 | }); 22 | -------------------------------------------------------------------------------- /src/cm/themes.js: -------------------------------------------------------------------------------- 1 | import * as prefs from '@/js/prefs'; 2 | import {FIREFOX} from '@/js/ua'; 3 | 4 | /** @type {{ [name: string]: string }} */ 5 | export const THEMES = __.THEMES; 6 | export const THEME_KEY = 'editor.theme'; 7 | const DEFAULT = 'default'; 8 | let EL; 9 | 10 | export async function loadCmTheme(name = prefs.__values[THEME_KEY]) { 11 | let css; 12 | if (name === DEFAULT) { 13 | css = ''; 14 | } else if ((css = THEMES[name]) == null) { 15 | css = ''; 16 | name = DEFAULT; 17 | prefs.set(THEME_KEY, name); 18 | } else if (!css) { 19 | css = `${__.CM_PATH}${name}.css`; 20 | if (!EL) { 21 | if (__.BUILD !== 'chrome' && FIREFOX) { 22 | EL = $tag('link'); 23 | EL.rel = 'stylesheet'; 24 | } else { 25 | EL = $tag('style'); 26 | } 27 | EL.id = 'cm-theme'; 28 | document.head.appendChild(EL); 29 | } 30 | // Firefox delays visual updates so we can fetch the theme asynchronously 31 | if (__.BUILD !== 'chrome' && FIREFOX) { 32 | EL.href = css; 33 | await new Promise(resolve => (EL.onload = resolve)); 34 | css = ''; 35 | } else { 36 | const xhr = new XMLHttpRequest(); 37 | xhr.open('GET', css, /*async=*/false); 38 | xhr.send(); 39 | css = THEMES[name] = xhr.response; 40 | } 41 | } 42 | if (EL) EL.textContent = css; 43 | } 44 | -------------------------------------------------------------------------------- /src/cm/util.js: -------------------------------------------------------------------------------- 1 | export function getStyleAtPos(styles, ch, pickOne) { 2 | if (!styles) return; 3 | const len = styles.length; 4 | const end = styles[len - 2]; 5 | if (ch > end) return; 6 | if (ch === end) { 7 | return pickOne === 0 ? styles[len - 1] 8 | : pickOne === 1 ? len - 2 9 | : [styles[len - 1], len - 2]; 10 | } 11 | const mid = (ch / end * (len - 1) & ~1) + 1; 12 | let a = mid; 13 | let b; 14 | while (a > 1 && styles[a] > ch) { 15 | b = a; 16 | a = (a / 2 & ~1) + 1; 17 | } 18 | if (!b) b = mid; 19 | while (b < len && styles[b] < ch) b = ((len + b) / 2 & ~1) + 1; 20 | while (a < b - 3) { 21 | const c = ((a + b) / 2 & ~1) + 1; 22 | if (styles[c] > ch) b = c; else a = c; 23 | } 24 | while (a < len && styles[a] < ch) a += 2; 25 | return pickOne === 0 ? styles[a + 1] 26 | : pickOne === 1 ? a 27 | : [styles[a + 1], a]; 28 | } 29 | -------------------------------------------------------------------------------- /src/content/install-hook-greasyfork.js: -------------------------------------------------------------------------------- 1 | /* global API */// msg.js 2 | 'use strict'; // eslint-disable-line strict 3 | 4 | // onCommitted may fire twice 5 | // Note, we're checking against a literal `1`, not just `if (truthy)`, 6 | // because is exposed per HTML spec as a global variable and `window.INJECTED`. 7 | 8 | if (window.INJECTED_GREASYFORK !== 1) { 9 | window.INJECTED_GREASYFORK = 1; 10 | addEventListener('message', async function onMessage(e) { 11 | if (e.origin === location.origin && 12 | e.data && 13 | e.data.name && 14 | e.data.type === 'style-version-query') { 15 | removeEventListener('message', onMessage); 16 | postMessage({ 17 | type: 'style-version', 18 | version: await API.usercss.getVersion(e.data), 19 | }, '*'); 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/content/install-hook-usercss.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // eslint-disable-line strict 2 | 3 | // preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case 4 | if (typeof window.oldCode !== 'string') { 5 | window.oldCode = (document.querySelector('body > pre') || document.body).textContent; 6 | chrome.runtime.onConnect.addListener(port => { 7 | if (port.name !== 'downloadSelf') return; 8 | port.onMessage.addListener(async ({id, force}) => { 9 | const msg = {id}; 10 | try { 11 | const code = await (await fetch(location.href, {mode: 'same-origin'})).text(); 12 | if (code !== window.oldCode || force) { 13 | msg.code = window.oldCode = code; 14 | } 15 | } catch (error) { 16 | msg.error = error.message || `${error}`; 17 | } 18 | port.postMessage(msg); 19 | }); 20 | // FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864 21 | addEventListener('pagehide', () => port.disconnect(), {once: true}); 22 | }); 23 | } 24 | 25 | // passing the result to tabs.executeScript 26 | oldCode; /* global oldCode*/// eslint-disable-line no-unused-expressions 27 | -------------------------------------------------------------------------------- /src/content/install-hook-userstylesworld.js: -------------------------------------------------------------------------------- 1 | /* global API */// msg.js 2 | 'use strict'; // eslint-disable-line strict 3 | 4 | if (window.USW !== 1) { 5 | window.USW = 1; // avoiding re-injection 6 | let filledInfo; 7 | const ORIGIN = location.origin; 8 | const send = (type, data) => postMessage({type, data}, ORIGIN); 9 | 10 | const onReady = async () => { 11 | send('usw-remove-stylus-button'); 12 | if (location.pathname === '/api/oauth/style/new') { 13 | filledInfo = true; 14 | const styleId = +new URLSearchParams(location.search).get('vendor_data'); 15 | const data = await API.data.get('usw' + styleId); 16 | send('usw-fill-new-style', data); 17 | } 18 | }; 19 | 20 | const HANDLERS = { 21 | __proto__: null, 22 | 'usw-ready': onReady, 23 | async 'usw-style-info-request'(data) { 24 | switch (data.requestType) { 25 | case 'installed': { 26 | const updateUrl = `${ORIGIN}/api/style/${data.styleID}.user.css`; 27 | const style = await API.styles.find({updateUrl}); 28 | data.installed = !!style; 29 | send('usw-style-info-response', data); 30 | break; 31 | } 32 | } 33 | }, 34 | }; 35 | 36 | if (!filledInfo) onReady(); 37 | addEventListener('message', ({data, origin}) => { 38 | if (chrome.runtime.id && data && origin === ORIGIN) { 39 | const fn = HANDLERS[data.type]; 40 | if (fn) fn(data); 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/css/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/css/icons.ttf -------------------------------------------------------------------------------- /src/css/onoffswitch.css: -------------------------------------------------------------------------------- 1 | .slider { 2 | --width: 24px; 3 | --knob: 12px; 4 | --pad: 0px; 5 | --color-off: hsla(0, 0%, 50%, .35); 6 | --color-on: hsla(180, 50%, 40%, .33); 7 | --shadow-hsl: 180, 50%, 10%; 8 | -webkit-appearance: none; 9 | -moz-appearance: none; 10 | appearance: none; 11 | border: none; 12 | flex: 0 0 var(--width); /* ensuring min/max width */ 13 | width: var(--width); 14 | height: calc(var(--knob) - 2 * var(--pad)); 15 | border-radius: var(--knob); 16 | color: var(--bg); 17 | background: var(--color-off); 18 | transition: box-shadow .2s; 19 | display: inline-flex; 20 | align-items: center; 21 | &, &:focus { 22 | box-shadow: inset 1px 1px 2px hsla(var(--shadow-hsl), .5); 23 | } 24 | &::after { 25 | content: ""; 26 | width: var(--knob); 27 | height: var(--knob); 28 | border-radius: 100%; 29 | box-shadow: 2px 2px 4px 1px hsla(var(--shadow-hsl), .4); 30 | margin: 0 calc(-1 * var(--pad)); 31 | background-color: currentColor; 32 | border: 1px solid var(--color-off); 33 | box-sizing: border-box; 34 | } 35 | &:checked { 36 | background-color: var(--color-on); 37 | justify-content: flex-end; 38 | color: var(--accent-2); 39 | &::after { 40 | border-color: hsla(var(--shadow-hsl), .25); 41 | } 42 | } 43 | &:hover { 44 | box-shadow: inset 1px 1px 2px hsla(var(--shadow-hsl), .8); 45 | } 46 | &:focus { 47 | position: relative; 48 | &:not([data-focused-via-click])::before { 49 | content: ""; 50 | position: absolute; 51 | width: 100%; 52 | height: 100%; 53 | left: calc(-2 * var(--pad)); 54 | padding: calc(var(--pad) + 2px) calc(var(--pad) * 2); 55 | box-shadow: var(--focus-shadow); 56 | } 57 | } 58 | :root[data-ui-theme="dark"] & { 59 | --color-off: hsla(0, 0%, 50%, 0.6); 60 | --color-on: hsla(180, 50%, 60%, .3); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/css/spinner.css: -------------------------------------------------------------------------------- 1 | /* spinner: https://github.com/loadingio/css-spinner */ 2 | .lds-spinner { 3 | -moz-user-select: none; 4 | user-select: none; 5 | pointer-events: none; 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | width: 200px; /* don't change! use "transform: scale(.75)" */ 11 | height: 200px; /* don't change! use "transform: scale(.75)" */ 12 | margin: auto; 13 | animation: fadeout 1s reverse; 14 | animation-fill-mode: both; 15 | > * { 16 | left: 94px; 17 | top: 23px; 18 | position: absolute; 19 | animation: fadeout linear 1s infinite; 20 | animation-direction: reverse; 21 | background: currentColor; 22 | width: 12px; 23 | height: 34px; 24 | border-radius: 20%; 25 | transform-origin: 6px 77px; 26 | } 27 | > :nth-child(1) { 28 | transform: rotate(0deg); 29 | animation-delay: -0.916666666666667s; 30 | } 31 | > :nth-child(2) { 32 | transform: rotate(30deg); 33 | animation-delay: -0.833333333333333s; 34 | } 35 | > :nth-child(3) { 36 | transform: rotate(60deg); 37 | animation-delay: -0.75s; 38 | } 39 | > :nth-child(4) { 40 | transform: rotate(90deg); 41 | animation-delay: -0.666666666666667s; 42 | } 43 | > :nth-child(5) { 44 | transform: rotate(120deg); 45 | animation-delay: -0.583333333333333s; 46 | } 47 | > :nth-child(6) { 48 | transform: rotate(150deg); 49 | animation-delay: -0.5s; 50 | } 51 | > :nth-child(7) { 52 | transform: rotate(180deg); 53 | animation-delay: -0.416666666666667s; 54 | } 55 | > :nth-child(8) { 56 | transform: rotate(210deg); 57 | animation-delay: -0.333333333333333s; 58 | } 59 | > :nth-child(9) { 60 | transform: rotate(240deg); 61 | animation-delay: -0.25s; 62 | } 63 | > :nth-child(10) { 64 | transform: rotate(270deg); 65 | animation-delay: -0.166666666666667s; 66 | } 67 | > :nth-child(11) { 68 | transform: rotate(300deg); 69 | animation-delay: -0.083333333333333s; 70 | } 71 | > :nth-child(12) { 72 | transform: rotate(330deg); 73 | animation-delay: 0s; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/edit/applies-to.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /src/edit/autocomplete.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-hint { 2 | b { 3 | background-color: hsla(51, 100%, 50%, .25); 4 | } 5 | .colorview-swatch { 6 | vertical-align: middle; 7 | &::before { 8 | left: 0; 9 | width: 11px; 10 | height: 11px; 11 | } 12 | } 13 | } 14 | .CodeMirror-hint-active b { 15 | background: deepskyblue; 16 | } 17 | -------------------------------------------------------------------------------- /src/edit/colorpicker-helper.js: -------------------------------------------------------------------------------- 1 | import {CodeMirror, extraKeys} from '@/cm'; 2 | import * as prefs from '@/js/prefs'; 3 | import '@/js/color/color-view'; 4 | import {t} from '@/js/util'; 5 | import cmFactory from './codemirror-factory'; 6 | 7 | const {defaults, commands} = CodeMirror; 8 | const ECP = 'editor.colorpicker.'; 9 | const CP = 'colorpicker'; 10 | 11 | prefs.subscribe(ECP + 'hotkey', (id, hotkey) => { 12 | commands[CP] = invokeColorpicker; 13 | for (const key in extraKeys) { 14 | if (extraKeys[key] === CP) { 15 | delete extraKeys[key]; 16 | break; 17 | } 18 | } 19 | if (hotkey) { 20 | extraKeys[hotkey] = CP; 21 | } 22 | }, true); 23 | 24 | prefs.subscribe(ECP.slice(0, -1), (id, enabled) => { 25 | const keyName = prefs.__values[ECP + 'hotkey']; 26 | defaults[CP] = enabled; 27 | if (enabled) { 28 | if (keyName) { 29 | commands[CP] = invokeColorpicker; 30 | extraKeys[keyName] = CP; 31 | } 32 | defaults[CP] = { 33 | tooltip: t('colorpickerTooltip'), 34 | popup: { 35 | tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'), 36 | paletteLine: t('numberedLine'), 37 | paletteHint: t('colorpickerPaletteHint'), 38 | hexUppercase: prefs.__values[ECP + 'hexUppercase'], 39 | embedderCallback: state => { 40 | for (const k of ['hexUppercase', 'color']) 41 | if (state[k] !== prefs.__values[ECP + k]) 42 | prefs.set(ECP + k, state[k]); 43 | }, 44 | get maxHeight() { 45 | return prefs.__values[ECP + 'maxHeight']; 46 | }, 47 | set maxHeight(h) { 48 | prefs.set(ECP + 'maxHeight', h); 49 | }, 50 | }, 51 | }; 52 | } else { 53 | delete extraKeys[keyName]; 54 | } 55 | cmFactory.globalSetOption(CP, defaults[CP]); 56 | }, true); 57 | 58 | function invokeColorpicker(cm) { 59 | cm.state[CP].openPopup(prefs.__values[ECP + 'color']); 60 | } 61 | -------------------------------------------------------------------------------- /src/edit/compact-header.js: -------------------------------------------------------------------------------- 1 | import {$create} from '@/js/dom'; 2 | import {mqCompact} from '@/js/dom-init'; 3 | import {important} from '@/js/dom-util'; 4 | import {template} from '@/js/localization'; 5 | import editor from './editor'; 6 | 7 | const h = template.body.$('#header'); 8 | export const toggleSticky = val => h.classList.toggle('sticky', val); 9 | export let sticky; 10 | 11 | export default function CompactHeader() { 12 | // Set up mini-header on scroll 13 | const {isUsercss} = editor; 14 | const elHeader = $create('div', { 15 | style: important(` 16 | top: 0; 17 | height: 1px; 18 | position: absolute; 19 | visibility: hidden; 20 | `), 21 | }); 22 | const scroller = isUsercss ? $('.CodeMirror-scroll') : document.body; 23 | const xoRoot = isUsercss ? scroller : undefined; 24 | const xo = new IntersectionObserver(onScrolled, {root: xoRoot}); 25 | const elInfo = $('h1 a'); 26 | scroller.appendChild(elHeader); 27 | mqCompact(val => { 28 | if (val) { 29 | xo.observe(elHeader); 30 | $id('basic-info-name').after(elInfo); 31 | } else { 32 | xo.disconnect(); 33 | $('h1').append(elInfo); 34 | } 35 | }); 36 | 37 | /** @param {IntersectionObserverEntry[]} entries */ 38 | function onScrolled(entries) { 39 | sticky = !entries.pop().intersectionRatio; 40 | if (!isUsercss) scroller.style.paddingTop = sticky ? h.offsetHeight + 'px' : ''; 41 | toggleSticky(sticky); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/edit/dirty-reporter.js: -------------------------------------------------------------------------------- 1 | export default function DirtyReporter() { 2 | const data = new Map(); 3 | const listeners = new Set(); 4 | const dataListeners = new Set(); 5 | const notifyChange = wasDirty => { 6 | const isDirty = data.size > 0; 7 | const flipped = isDirty !== wasDirty; 8 | if (flipped) { 9 | listeners.forEach(cb => cb(isDirty)); 10 | } 11 | if (flipped || isDirty) { 12 | dataListeners.forEach(cb => cb(isDirty)); 13 | } 14 | }; 15 | /** @namespace DirtyReporter */ 16 | return { 17 | add(obj, value) { 18 | const wasDirty = data.size > 0; 19 | const saved = data.get(obj); 20 | if (!saved) { 21 | data.set(obj, {type: 'add', newValue: value}); 22 | } else if (saved.type === 'remove') { 23 | if (saved.savedValue === value) { 24 | data.delete(obj); 25 | } else { 26 | saved.newValue = value; 27 | saved.type = 'modify'; 28 | } 29 | } else { 30 | return; 31 | } 32 | notifyChange(wasDirty); 33 | }, 34 | clear(id) { 35 | if (data.size && ( 36 | id ? data.delete(id) 37 | : (data.clear(), true) 38 | )) { 39 | notifyChange(true); 40 | } 41 | }, 42 | has(key) { 43 | return data.has(key); 44 | }, 45 | isDirty() { 46 | return data.size > 0; 47 | }, 48 | modify(obj, oldValue, newValue) { 49 | const wasDirty = data.size > 0; 50 | const saved = data.get(obj); 51 | if (!saved) { 52 | if (oldValue !== newValue) { 53 | data.set(obj, {type: 'modify', savedValue: oldValue, newValue}); 54 | } else { 55 | return; 56 | } 57 | } else if (saved.type === 'modify') { 58 | if (saved.savedValue === newValue) { 59 | data.delete(obj); 60 | } else { 61 | saved.newValue = newValue; 62 | } 63 | } else if (saved.type === 'add') { 64 | saved.newValue = newValue; 65 | } else { 66 | return; 67 | } 68 | notifyChange(wasDirty); 69 | }, 70 | onChange(cb, add = true) { 71 | listeners[add ? 'add' : 'delete'](cb); 72 | }, 73 | onDataChange(cb, add = true) { 74 | dataListeners[add ? 'add' : 'delete'](cb); 75 | }, 76 | remove(obj, value) { 77 | const wasDirty = data.size > 0; 78 | const saved = data.get(obj); 79 | if (!saved) { 80 | data.set(obj, {type: 'remove', savedValue: value}); 81 | } else if (saved.type === 'add') { 82 | data.delete(obj); 83 | } else if (saved.type === 'modify') { 84 | saved.type = 'remove'; 85 | } else { 86 | return; 87 | } 88 | notifyChange(wasDirty); 89 | }, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/edit/drafts.js: -------------------------------------------------------------------------------- 1 | import {$create} from '@/js/dom'; 2 | import {formatRelativeDate} from '@/js/localization'; 3 | import {API} from '@/js/msg-api'; 4 | import * as prefs from '@/js/prefs'; 5 | import {styleToCss} from '@/js/sections-util'; 6 | import {clamp, debounce, t} from '@/js/util'; 7 | import editor from './editor'; 8 | import {helpPopup, showCodeMirrorPopup} from './util'; 9 | 10 | const makeId = () => editor.style.id || 'new'; 11 | let delay; 12 | let port; 13 | 14 | maybeRestore().then(() => { 15 | editor.dirty.onChange(isDirty => isDirty ? !port && connectPort() : port?.disconnect()); 16 | editor.dirty.onDataChange(isDirty => debounce(updateDraft, isDirty ? delay : 0)); 17 | prefs.subscribe('editor.autosaveDraft', (key, val) => { 18 | delay = clamp(val * 1000 | 0, 1000, 2 ** 32 - 1); 19 | const timer = debounce.timers.get(updateDraft); 20 | if (timer) debounce(updateDraft, timer.delay ? delay : 0); 21 | }, true); 22 | }); 23 | 24 | async function maybeRestore() { 25 | const draft = await API.draftsDB.get(makeId()); 26 | if (!draft || draft.isUsercss !== editor.isUsercss || editor.isSame(draft.style)) { 27 | return; 28 | } 29 | let resolve; 30 | const {style} = draft; 31 | const onYes = () => resolve(true); 32 | const onNo = () => resolve(false); 33 | const value = draft.isUsercss ? style.sourceCode : styleToCss(style); 34 | const info = t('draftTitle', formatRelativeDate(draft.date)); 35 | const popup = showCodeMirrorPopup(info, '', {value, readOnly: true}); 36 | popup.className += ' danger'; 37 | popup.onClose.add(onNo); 38 | popup._contents.append( 39 | $create('p', t('draftAction')), 40 | $create('.buttons', [t('confirmYes'), t('confirmNo')].map((btn, i) => 41 | $create('button', {onclick: i ? onNo : onYes}, btn))) 42 | ); 43 | if (await new Promise(r => (resolve = r))) { 44 | style.id = editor.style.id; 45 | await editor.replaceStyle(style, draft); 46 | } else { 47 | API.draftsDB.delete(makeId()).catch(() => {}); 48 | } 49 | helpPopup.close(); 50 | } 51 | 52 | function connectPort() { 53 | port = chrome.runtime.connect({name: 'draft:' + makeId()}); 54 | port.onDisconnect.addListener(() => (port = null)); 55 | } 56 | 57 | function updateDraft(isDirty = editor.dirty.isDirty()) { 58 | if (!isDirty) return; 59 | API.draftsDB.put({ 60 | date: new Date(), 61 | isUsercss: editor.isUsercss, 62 | style: editor.getValue(true), 63 | si: editor.makeScrollInfo(), 64 | }, makeId()); 65 | } 66 | -------------------------------------------------------------------------------- /src/edit/editor-header.js: -------------------------------------------------------------------------------- 1 | import {CodeMirror, extraKeys} from '@/cm'; 2 | import {setInputValue, setupLivePrefs} from '@/js/dom-util'; 3 | import * as prefs from '@/js/prefs'; 4 | import {sleep, t} from '@/js/util'; 5 | import {initBeautifyButton} from './beautify'; 6 | import editor from './editor'; 7 | 8 | export default function EditorHeader() { 9 | initBeautifyButton($id('beautify')); 10 | initNameArea(); 11 | setupLivePrefs(); 12 | window.on('load', () => { 13 | prefs.subscribe('editor.keyMap', showHotkeyInTooltip, true); 14 | window.on('showHotkeyInTooltip', showHotkeyInTooltip); 15 | }, {once: true}); 16 | for (const el of $$('#header summary')) { 17 | el.on('contextmenu', peekDetails); 18 | } 19 | } 20 | 21 | function findKeyForCommand(command, map) { 22 | if (typeof map === 'string') map = CodeMirror.keyMap[map]; 23 | let key = Object.keys(map).find(k => map[k] === command); 24 | if (key) { 25 | return key; 26 | } 27 | for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) { 28 | key = ft && findKeyForCommand(command, ft); 29 | if (key) { 30 | return key; 31 | } 32 | } 33 | return ''; 34 | } 35 | 36 | function initNameArea() { 37 | const nameEl = $id('name'); 38 | const resetEl = $id('reset-name'); 39 | const isCustomName = editor.style.updateUrl || editor.isUsercss; 40 | editor.nameTarget = isCustomName ? 'customName' : 'name'; 41 | nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); 42 | nameEl.title = isCustomName ? t('customNameHint') : ''; 43 | nameEl.on('input', () => { 44 | editor.updateName(true); 45 | resetEl.hidden = !editor.style.customName; 46 | }); 47 | resetEl.hidden = !editor.style.customName; 48 | resetEl.onclick = () => { 49 | setInputValue(nameEl, editor.style.name); 50 | editor.style.customName = null; // to delete it from db 51 | resetEl.hidden = true; 52 | }; 53 | const enabledEl = $id('enabled'); 54 | enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked); 55 | } 56 | 57 | async function peekDetails(evt) { 58 | evt.preventDefault(); 59 | const elDetails = this.parentNode; 60 | if (!(elDetails.open = !elDetails.open)) return; 61 | while (elDetails.matches(':hover, :active')) { 62 | await sleep(500); 63 | await new Promise(cb => elDetails.on('mouseleave', cb, {once: true})); 64 | } 65 | elDetails.open = false; 66 | } 67 | 68 | function showHotkeyInTooltip(_, mapName = prefs.__values['editor.keyMap']) { 69 | for (const el of $$('[data-hotkey-tooltip]')) { 70 | if (el._hotkeyTooltipKeyMap !== mapName) { 71 | el._hotkeyTooltipKeyMap = mapName; 72 | const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title; 73 | const cmd = el.dataset.hotkeyTooltip; 74 | const key = cmd[0] === '=' ? cmd.slice(1) : 75 | findKeyForCommand(cmd, mapName) || 76 | findKeyForCommand(cmd, extraKeys); 77 | const newTitle = title + (title && key ? '\n' : '') + (key || ''); 78 | if (el.title !== newTitle) el.title = newTitle; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/edit/editor-settings.html: -------------------------------------------------------------------------------- 1 |
    2 | 5 | 8 | 11 | 14 | 17 | 20 | 23 |
    24 | 27 | 28 | 29 | 30 |
    31 | 34 | 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 | 66 |
    67 |
    68 |
    69 | 70 |
    71 | 76 |
    77 | 78 | 79 | 80 |
    81 |
    82 | -------------------------------------------------------------------------------- /src/edit/embedded-popup.js: -------------------------------------------------------------------------------- 1 | import {extraKeys} from '@/cm'; 2 | import {$create} from '@/js/dom'; 3 | import {getEventKeyName} from '@/js/dom-util'; 4 | import * as prefs from '@/js/prefs'; 5 | import {actionPopupUrl} from '@/js/urls'; 6 | import {t} from '@/js/util'; 7 | import {MF_ICON_EXT, MF_ICON_PATH} from '@/js/util-webext'; 8 | 9 | export default function EmbeddedPopup() { 10 | const ID = 'popup-iframe'; 11 | const POPUP_HOTKEY = 'Shift-Ctrl-Alt-S'; 12 | /** @type {HTMLIFrameElement} */ 13 | let frame; 14 | let isLoaded; 15 | let scrollbarWidth; 16 | 17 | const btn = $create('img', { 18 | id: 'popup-button', 19 | title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY, 20 | onclick: embedPopup, 21 | }); 22 | $root.appendChild(btn); 23 | $rootCL.add('popup-window'); 24 | document.body.appendChild(btn); 25 | // Adding a dummy command to show in keymap help popup 26 | extraKeys[POPUP_HOTKEY] = 'openStylusPopup'; 27 | 28 | prefs.subscribe('iconset', (_, val) => { 29 | const prefix = `${MF_ICON_PATH}${val ? 'light/' : ''}`; 30 | btn.srcset = `${prefix}16${MF_ICON_EXT} 1x,${prefix}32${MF_ICON_EXT} 2x`; 31 | }, true); 32 | 33 | window.on('keydown', e => { 34 | if (getEventKeyName(e) === POPUP_HOTKEY) { 35 | embedPopup(); 36 | } 37 | }); 38 | 39 | function embedPopup() { 40 | if ($id(ID)) return; 41 | isLoaded = false; 42 | scrollbarWidth = 0; 43 | frame = $create('iframe', { 44 | id: ID, 45 | src: actionPopupUrl, 46 | height: 600, 47 | width: prefs.__values.popupWidth, 48 | onload: initFrame, 49 | }); 50 | window.on('mousedown', removePopup); 51 | document.body.appendChild(frame); 52 | } 53 | 54 | function initFrame() { 55 | frame = this; 56 | frame.focus(); 57 | const pw = frame.contentWindow; 58 | const body = pw.document.body; 59 | pw.on('keydown', removePopupOnEsc); 60 | pw.close = removePopup; 61 | new pw.IntersectionObserver(onIntersect).observe(body.appendChild( 62 | $create('div', {style: 'height: 1px; marginTop: -1px;'}) 63 | )); 64 | new pw.MutationObserver(onMutation).observe(body, { 65 | attributes: true, 66 | attributeFilter: ['style'], 67 | }); 68 | } 69 | 70 | function onMutation() { 71 | const body = frame.contentDocument.body; 72 | const bs = body.style; 73 | const w = parseFloat(bs.minWidth || bs.width) + (scrollbarWidth || 0); 74 | const h = parseFloat(bs.minHeight || body.offsetHeight); 75 | if (frame.width - w) frame.width = w; 76 | if (frame.height - h) frame.height = h; 77 | } 78 | 79 | function onIntersect([e]) { 80 | const pw = frame.contentWindow; 81 | const el = pw.document.scrollingElement; 82 | const h = e.intersectionRatio && !pw.scrollY ? el.offsetHeight : el.scrollHeight; 83 | const hasSB = h > el.offsetHeight; 84 | const {width} = e.boundingClientRect; 85 | frame.height = h; 86 | if (!hasSB !== !scrollbarWidth || frame.width - width) { 87 | scrollbarWidth = hasSB ? width - el.offsetWidth : 0; 88 | frame.width = width + scrollbarWidth; 89 | } 90 | if (!isLoaded) { 91 | isLoaded = true; 92 | frame.dataset.loaded = ''; 93 | } 94 | } 95 | 96 | function removePopup() { 97 | frame = null; 98 | $id(ID)?.remove(); 99 | window.off('mousedown', removePopup); 100 | } 101 | 102 | function removePopupOnEsc(e) { 103 | if (getEventKeyName(e) === 'Escape') { 104 | removePopup(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/edit/global-search.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | 31 | 39 | 40 | 72 | -------------------------------------------------------------------------------- /src/edit/index.js: -------------------------------------------------------------------------------- 1 | import '@/js/dom-init'; 2 | import {tBody} from '@/js/localization'; 3 | import * as prefs from '@/js/prefs'; 4 | import {CodeMirror} from '@/cm'; 5 | import CompactHeader, {toggleSticky} from './compact-header'; 6 | import editor from './editor'; 7 | import EditorHeader from './editor-header'; 8 | import * as linterMan from './linter'; 9 | import loading from './load-style'; 10 | import './on-msg-extension'; 11 | import './settings'; 12 | import SectionsEditor from './sections-editor'; 13 | import SourceEditor from './source-editor'; 14 | import './colorpicker-helper'; 15 | import './live-preview'; 16 | import USWIntegration from './usw-integration'; 17 | import './windowed-mode'; 18 | import './edit.css'; 19 | /** Loading here to avoid a separate tiny file in dist */ 20 | import './autocomplete.css'; 21 | 22 | tBody(); 23 | 24 | (async () => { 25 | if (loading) await loading; 26 | if (editor.scrollInfo.sticky) toggleSticky(true); 27 | EditorHeader(); 28 | USWIntegration(); 29 | // TODO: load respective js on demand? 30 | (editor.isUsercss ? SourceEditor : SectionsEditor)(); 31 | editor.dirty.onChange(editor.updateDirty); 32 | prefs.subscribe('editor.linter', () => linterMan.run()); 33 | CompactHeader(); 34 | import('./lazy-init'); 35 | const cmCommands = CodeMirror.commands; 36 | for (const cmd of ['find', 'findNext', 'findPrev', 'replace', 'replaceAll']) { 37 | cmCommands[cmd] = async (...args) => { 38 | await (await import('./global-search')).init(); 39 | cmCommands[cmd](...args); 40 | }; 41 | } 42 | // enabling after init to prevent flash of validation failure on an empty name 43 | $id('name').required = !editor.isUsercss; 44 | $id('save-button').onclick = editor.save; 45 | $id('cancel-button').onclick = editor.cancel; 46 | // $id('testRE').hidden = !editor.style.sections.some(({regexps: r}) => r && r.length); 47 | $id('testRE').onclick = async function () { 48 | (this.onclick = (await import('./regexp-tester')).toggle)(true); 49 | }; 50 | $id('lint-help').onclick = async function () { 51 | (this.onclick = (await import('./linter/dialogs')).showLintHelp)(); 52 | }; 53 | const elSec = $id('sections-list'); 54 | const elToc = $id('toc'); 55 | const moDetails = new MutationObserver(([{target: sec}]) => { 56 | if (!sec.open) return; 57 | if (sec === elSec) editor.updateToc(); 58 | const el = sec.lastElementChild; 59 | const s = el.style; 60 | const x2 = sec.getBoundingClientRect().left + el.getBoundingClientRect().width; 61 | if (x2 > innerWidth - 30) s.right = '0'; 62 | else if (s.right) s.removeProperty('right'); 63 | }); 64 | // editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work 65 | if (elSec.open) editor.updateToc(); 66 | // and we also toggle `open` directly in other places e.g. in detectLayout() 67 | for (const el of $$('#details-wrapper > details')) { 68 | moDetails.observe(el, {attributes: true, attributeFilter: ['open']}); 69 | } 70 | elToc.onclick = e => 71 | editor.jumpToEditor([].indexOf.call(elToc.children, e.target)); 72 | })(); 73 | -------------------------------------------------------------------------------- /src/edit/keymap-help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | -------------------------------------------------------------------------------- /src/edit/lazy-init.js: -------------------------------------------------------------------------------- 1 | import './autocomplete'; 2 | import './drafts'; 3 | import './unload'; 4 | -------------------------------------------------------------------------------- /src/edit/linter/defaults.js: -------------------------------------------------------------------------------- 1 | const WARNING = {severity: 'warning'}; 2 | const ENABLED_AS_WARNING = [true, WARNING]; 3 | export const DEFAULTS = { 4 | stylelint: { 5 | // WARNING! onConfigSave() expects these rules to be arrays and enabled. 6 | // TODO: extract deduplicateRules and use it in onInstalled event + in tests 7 | rules: { 8 | 'at-rule-no-unknown': [true, { 9 | 'ignoreAtRules': ['extend', 'extends', 'css', 'block'], 10 | ...WARNING, 11 | }], 12 | 'block-no-empty': ENABLED_AS_WARNING, 13 | 'color-no-invalid-hex': ENABLED_AS_WARNING, 14 | 'declaration-block-no-duplicate-properties': [true, { 15 | 'ignore': ['consecutive-duplicates-with-different-values'], 16 | ...WARNING, 17 | }], 18 | 'declaration-block-no-shorthand-property-overrides': ENABLED_AS_WARNING, 19 | 'font-family-no-duplicate-names': ENABLED_AS_WARNING, 20 | 'function-calc-no-unspaced-operator': ENABLED_AS_WARNING, 21 | 'function-linear-gradient-no-nonstandard-direction': ENABLED_AS_WARNING, 22 | 'keyframe-declaration-no-important': ENABLED_AS_WARNING, 23 | 'media-feature-name-no-unknown': ENABLED_AS_WARNING, 24 | 'no-invalid-double-slash-comments': ENABLED_AS_WARNING, 25 | 'property-no-unknown': ENABLED_AS_WARNING, 26 | 'selector-pseudo-class-no-unknown': ENABLED_AS_WARNING, 27 | 'selector-pseudo-element-no-unknown': ENABLED_AS_WARNING, 28 | 'string-no-newline': ENABLED_AS_WARNING, 29 | 'unit-no-unknown': ENABLED_AS_WARNING, 30 | }, 31 | }, 32 | csslint: { 33 | 'display-property-grouping': 1, 34 | 'duplicate-properties': 1, 35 | 'empty-rules': 1, 36 | 'errors': 1, 37 | 'globals-in-document': 1, 38 | 'known-properties': 1, 39 | 'known-pseudos': 1, 40 | 'selector-newline': 1, 41 | 'shorthand-overrides': 1, 42 | 'simple-not': 1, 43 | 'warnings': 1, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/edit/linter/engines.js: -------------------------------------------------------------------------------- 1 | import {getLZValue, LZ_KEY} from '@/js/chrome-sync'; 2 | import * as prefs from '@/js/prefs'; 3 | import {onStorageChanged} from '@/js/util-webext'; 4 | import * as linterMan from '.'; 5 | import editor from '../editor'; 6 | import {worker} from '../util'; 7 | import {DEFAULTS} from './defaults'; 8 | 9 | const configs = new Map(); 10 | const ENGINES = { 11 | csslint: { 12 | validMode: mode => mode === 'css', 13 | getConfig: config => Object.assign({}, DEFAULTS.csslint, config), 14 | async lint(text, config) { 15 | config.doc = !editor.isUsercss; 16 | const results = await worker.csslint(text, config); 17 | return results 18 | .map(({line, col: ch, message, rule, type: severity}) => line && { 19 | message, 20 | from: {line: line - 1, ch: ch - 1}, 21 | to: {line: line - 1, ch}, 22 | rule: rule.id, 23 | severity, 24 | }) 25 | .filter(Boolean); 26 | }, 27 | }, 28 | stylelint: { 29 | validMode: () => true, 30 | getConfig: config => ({ 31 | rules: Object.assign({}, DEFAULTS.stylelint.rules, config && config.rules), 32 | }), 33 | lint: (code, config, mode) => worker.stylelint({code, config, mode}), 34 | }, 35 | }; 36 | 37 | linterMan.register(async (text, _options, cm) => { 38 | const linter = prefs.__values['editor.linter']; 39 | if (linter) { 40 | const {mode} = cm.options; 41 | const currentFirst = Object.entries(ENGINES).sort(([a]) => a === linter ? -1 : 1); 42 | for (const [name, engine] of currentFirst) { 43 | if (engine.validMode(mode)) { 44 | const cfg = configs.get(name) || await getConfig(name); 45 | return ENGINES[name].lint(text, cfg, mode); 46 | } 47 | } 48 | } 49 | }); 50 | 51 | onStorageChanged.addListener(changes => { 52 | for (const name of Object.keys(ENGINES)) { 53 | if (LZ_KEY[name] in changes) { 54 | getConfig(name).then(linterMan.run); 55 | } 56 | } 57 | }); 58 | 59 | async function getConfig(name) { 60 | const rawCfg = await getLZValue(LZ_KEY[name]); 61 | const cfg = ENGINES[name].getConfig(rawCfg); 62 | configs.set(name, cfg); 63 | return cfg; 64 | } 65 | -------------------------------------------------------------------------------- /src/edit/linter/index.js: -------------------------------------------------------------------------------- 1 | import {cms, linters, lintingUpdatedListeners, unhookListeners} from './store'; 2 | import './engines'; 3 | 4 | export * from './defaults'; 5 | export * from './reports'; 6 | 7 | export function disableForEditor(cm) { 8 | cm.setOption('lint', false); 9 | cms.delete(cm); 10 | for (const cb of unhookListeners) { 11 | cb(cm); 12 | } 13 | } 14 | 15 | /** 16 | * @param {Object} cm 17 | * @param {string} [code] - to be used to avoid slowdowns when creating a lot of cms. 18 | * Enables lint option only if there are problems, thus avoiding a _very_ costly layout 19 | * update when lint gutter is added to a lot of editors simultaneously. 20 | */ 21 | export function enableForEditor(cm, code) { 22 | if (cms.has(cm)) return; 23 | cms.set(cm, null); 24 | if (code) { 25 | enableOnProblems(cm, code); 26 | } else { 27 | cm.setOption('lint', {getAnnotations, onUpdateLinting}); 28 | } 29 | } 30 | 31 | export function onLintingUpdated(fn) { 32 | lintingUpdatedListeners.push(fn); 33 | } 34 | 35 | export function onUnhook(fn) { 36 | unhookListeners.push(fn); 37 | } 38 | 39 | export function register(fn) { 40 | linters.push(fn); 41 | } 42 | 43 | export function run() { 44 | for (const cm of cms.keys()) { 45 | cm.performLint(); 46 | } 47 | } 48 | 49 | async function enableOnProblems(cm, code) { 50 | const results = await getAnnotations(code, {}, cm); 51 | if (results.length || cm.display.renderedView) { 52 | cms.set(cm, results); 53 | cm.setOption('lint', {getAnnotations: getCachedAnnotations, onUpdateLinting}); 54 | } else { 55 | cms.delete(cm); 56 | } 57 | } 58 | 59 | async function getAnnotations(...args) { 60 | const results = await Promise.all(linters.map(fn => fn(...args))); 61 | return [].concat(...results.filter(Boolean)); 62 | } 63 | 64 | function getCachedAnnotations(code, opt, cm) { 65 | const results = cms.get(cm); 66 | cms.set(cm, null); 67 | cm.state.lint.options.getAnnotations = getAnnotations; 68 | return results; 69 | } 70 | 71 | function onUpdateLinting(...args) { 72 | for (const fn of lintingUpdatedListeners) { 73 | fn(...args); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/edit/linter/reports.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
    :
    12 |
    13 | -------------------------------------------------------------------------------- /src/edit/linter/store.js: -------------------------------------------------------------------------------- 1 | export const cms = new Map(); 2 | export const linters = []; 3 | export const lintingUpdatedListeners = []; 4 | export const unhookListeners = []; 5 | -------------------------------------------------------------------------------- /src/edit/live-preview.js: -------------------------------------------------------------------------------- 1 | import {UCD} from '@/js/consts'; 2 | import {API} from '@/js/msg-api'; 3 | import * as prefs from '@/js/prefs'; 4 | import editor from './editor'; 5 | 6 | const ID = 'editor.livePreview'; 7 | let errPos; 8 | let el; 9 | let data; 10 | let port; 11 | let enabled; 12 | 13 | prefs.subscribe(ID, (key, value, init) => { 14 | enabled = value; 15 | if (init) return; 16 | if (!value) { 17 | if (port) { 18 | port.disconnect(); 19 | port = null; 20 | } 21 | } else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) { 22 | createPreviewer(); 23 | updatePreviewer(data); 24 | } 25 | }, true); 26 | 27 | editor.livePreview = newData => { 28 | if (!port) { 29 | if (!enabled 30 | || !newData.id // not saved 31 | || !newData.enabled && data && !data.enabled // disabled both before and now 32 | || !editor.dirty.isDirty()) { 33 | return; 34 | } 35 | createPreviewer(); 36 | } 37 | data = newData; 38 | updatePreviewer(data); 39 | }; 40 | 41 | function createPreviewer() { 42 | port = chrome.runtime.connect({name: 'livePreview:' + editor.style.id}); 43 | port.onDisconnect.addListener(() => (port = null)); 44 | el = $id('preview-errors'); 45 | el.onclick = showError; 46 | } 47 | 48 | function showError() { 49 | if (errPos) { 50 | const cm = editor.getEditors()[0]; 51 | cm.jumpToPos(errPos); 52 | cm.focus(); 53 | } 54 | } 55 | 56 | async function updatePreviewer(newData) { 57 | try { 58 | await API.styles.preview(newData); 59 | el.hidden = true; 60 | } catch (err) { 61 | const ucd = newData[UCD]; 62 | const pp = ucd && ucd.preprocessor; 63 | const shift = err._varLines + 1 || 0; 64 | errPos = pp && (err.line ??= err.lineno) && err.column 65 | ? {line: err.line - shift, ch: err.column - 1} 66 | : err.index; 67 | if (Array.isArray(err)) { 68 | err = err.map((e, a, b) => !(a = e.message) ? e : ((b = e.context)) ? `${a} in ${b}` : a) 69 | .join('\n'); 70 | } else { 71 | err = err.message || `${err}`; 72 | } 73 | if (errPos >= 0) { 74 | // FIXME: this would fail if editors[0].getValue() !== data.sourceCode 75 | errPos = editor.getEditors()[0].posFromIndex(errPos); 76 | } else if (!errPos && pp === 'stylus' && ( 77 | errPos = err.match(/^\w+:(\d+):(\d+)(?:\n.+)+\s+(.+)/) 78 | )) { 79 | err = errPos[3]; 80 | errPos = {line: errPos[1] - shift, ch: errPos[2] - 1}; 81 | } 82 | el.title = 83 | el.firstChild.textContent = (errPos ? `${errPos.line + 1}:${errPos.ch + 1} ` : '') + err; 84 | el.lastChild.hidden = !(el.lastChild.href = editor.ppDemo[pp]); 85 | el.hidden = false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/edit/load-style.js: -------------------------------------------------------------------------------- 1 | import {loadCmTheme} from '@/cm'; 2 | import * as prefs from '@/js/prefs'; 3 | import {FROM_CSS} from '@/js/sections-util'; 4 | import {clipString, sessionStore, tryURL} from '@/js/util'; 5 | import editor from './editor'; 6 | 7 | if (location.hash) { // redirected from devtools -> "open in a new tab" 8 | history.replaceState(history.state, '', location.href.split('#')[0]); 9 | } 10 | 11 | const params = new URLSearchParams(location.search); 12 | let id = +params.get('id'); 13 | 14 | export default __.MV3 15 | ? loadStyle(prefs.clientData) 16 | : prefs.clientData.then(loadStyle); 17 | 18 | function loadStyle({style = makeNewStyleObj(), isUC, si, template}) { 19 | Object.assign(editor, /** @namespace Editor */ { 20 | style, 21 | template, 22 | isUsercss: isUC, 23 | scrollInfo: si || {}, 24 | }); 25 | editor.updateClass(); 26 | editor.updateTitle(false); 27 | $rootCL.add(isUC ? 'usercss' : 'sectioned'); 28 | sessionStore.justEditedStyleId = id || ''; 29 | // no such style so let's clear the invalid URL parameters 30 | if (id === null) { 31 | params.delete('id'); 32 | const str = `${params}`; 33 | history.replaceState({}, '', location.pathname + (str ? '?' : '') + str); 34 | } 35 | return loadCmTheme(); 36 | } 37 | 38 | function makeNewStyleObj() { 39 | id = null; // resetting the non-existent id 40 | const prefix = tryURL(params.get('url-prefix')); 41 | const name = params.get('name') || prefix.hostname; 42 | const p = prefix.pathname || '/'; 43 | const section = {code: ''}; 44 | for (let [k, v] of params) if ((k = FROM_CSS[k])) section[k] = [v]; 45 | return { 46 | id, 47 | enabled: true, 48 | name: name 49 | ? name + (p === '/' ? '' : clipString(p.replace(/\.(html?|aspx?|cgi|php)$/, ''))) 50 | : params.get('domain') || '?', 51 | sections: [section], 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/edit/on-msg-extension.js: -------------------------------------------------------------------------------- 1 | import {onMessage} from '@/js/msg'; 2 | import {API} from '@/js/msg-api'; 3 | import {closeCurrentTab} from '@/js/util-webext'; 4 | import editor from './editor'; 5 | 6 | onMessage.set(request => { 7 | const {style} = request; 8 | switch (request.method) { 9 | case 'styleUpdated': 10 | if (editor.style.id === style.id) { 11 | handleExternalUpdate(request); 12 | } 13 | break; 14 | case 'styleDeleted': 15 | if (editor.style.id === style.id) { 16 | closeCurrentTab(); 17 | } 18 | break; 19 | } 20 | }); 21 | 22 | async function handleExternalUpdate({style, reason}) { 23 | if (reason === 'editPreview' || 24 | reason === 'editPreviewEnd') { 25 | return; 26 | } 27 | if (reason === 'editSave' && editor.saving) { 28 | editor.saving = false; 29 | return; 30 | } 31 | if (reason === 'toggle') { 32 | if (editor.dirty.isDirty()) { 33 | editor.toggleStyle(style.enabled); 34 | // updateLivePreview is called by toggleStyle 35 | } else { 36 | Object.assign(editor.style, style); 37 | editor.updateLivePreview(); 38 | } 39 | editor.updateMeta(); 40 | return; 41 | } 42 | style = await API.styles.getCore({id: style.id, vars: true}); 43 | if (reason === 'config') { 44 | for (const key in editor.style) 45 | if (key !== 'sourceCode' && key !== 'sections' && !(key in style)) 46 | delete editor.style[key]; 47 | delete style.name; 48 | delete style.enabled; 49 | Object.assign(editor.style, style); 50 | editor.updateLivePreview(); 51 | } else { 52 | await editor.replaceStyle(style); 53 | } 54 | window.dispatchEvent(new Event('styleSettings')); 55 | } 56 | -------------------------------------------------------------------------------- /src/edit/regexp-tester.css: -------------------------------------------------------------------------------- 1 | .regexp-report { 2 | &#help-popup { 3 | max-width: 50vw; 4 | } 5 | & h3 { 6 | margin-top: 0; 7 | margin-left: calc(-1 * var(--pad)); 8 | &::after { 9 | content: " (" attr(data-num) ")"; 10 | } 11 | } 12 | & h4 { 13 | cursor: default; 14 | counter-increment: rx; 15 | margin: var(--pad) 0 var(--pad05) calc(-1 * var(--pad05)); 16 | } 17 | & details > a::before, 18 | & h4::before { 19 | content: counter(rx) ". "; 20 | } 21 | & mark { 22 | background-color: rgba(255, 255, 0, .5); 23 | } 24 | & summary { 25 | margin-left: calc(-2ch - var(--pad)); 26 | } 27 | & details { 28 | word-break: break-all; 29 | overflow-wrap: break-word; 30 | margin-bottom: var(--pad); 31 | padding-left: var(--pad); 32 | counter-reset: rx; 33 | &[data-type="full"] { 34 | color: var(--accent-1); 35 | } 36 | &[data-type="partial"] { 37 | color: var(--c65); 38 | } 39 | &[data-type="invalid"] { 40 | color: var(--red1); 41 | } 42 | & h3 { 43 | display: inline-block; 44 | margin: 0 0 var(--pad05) 0; 45 | } 46 | & > a { 47 | counter-increment: rx; 48 | } 49 | } 50 | & article { 51 | padding-left: var(--pad05); 52 | } 53 | & a { 54 | color: inherit; 55 | cursor: default; 56 | white-space: nowrap; 57 | text-overflow: ellipsis; 58 | overflow: hidden; 59 | display: block; 60 | & img { 61 | width: 16px; 62 | height: 16px; 63 | margin-right: .25em; 64 | object-fit: contain; 65 | vertical-align: text-bottom; 66 | } 67 | } 68 | & :is(h4, a):hover { 69 | text-decoration: underline; 70 | } 71 | } 72 | .regexp-report-note { 73 | color: var(--c60); 74 | min-width: fit-content; 75 | width: 0; 76 | hyphens: auto; 77 | & code { 78 | white-space: nowrap; 79 | font-weight: bold; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/edit/sections-editor.html: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/edit/settings.css: -------------------------------------------------------------------------------- 1 | /* postcss-simple-vars */ 2 | .settings { 3 | &.dirty > summary > ::after { 4 | content: ' *'; 5 | } 6 | > main { 7 | > * { 8 | display: block; 9 | margin: 1rem 0; 10 | padding: 0; 11 | } 12 | > :first-child { 13 | margin-top: 0; 14 | } 15 | > :last-child { 16 | margin-bottom: 0; 17 | } 18 | } 19 | input:disabled ~ label { 20 | opacity: .5; 21 | } 22 | .w100 { 23 | display: block; 24 | width: 100%; 25 | margin-top: .25em; 26 | box-sizing: border-box; 27 | } 28 | textarea { 29 | $height: 1.75em; 30 | resize: vertical; 31 | min-height: $height; 32 | max-height: 50vh; 33 | white-space: pre; 34 | &:placeholder-shown { 35 | resize: none; 36 | height: 1.75em; 37 | &:not(:focus) { 38 | background: none; 39 | border: transparent; 40 | } 41 | } 42 | } 43 | .compact-layout & .radio-wrapper { 44 | display: inline-flex; 45 | padding: 0 .8em 0 0; 46 | } 47 | a[data-cmd=note] { 48 | vertical-align: text-bottom; 49 | } 50 | 51 | /* editor settings */ 52 | > main > section:not(.aligned) > * { 53 | display: flex; 54 | align-items: center; 55 | margin: 0; 56 | padding: .15em 0; 57 | } 58 | } 59 | /* any aligned settings */ 60 | section.aligned { 61 | > * { 62 | display: table-row; 63 | } 64 | > * > :not(.icon) { 65 | display: table-cell; 66 | margin-top: 0.1rem; 67 | min-height: 1.4rem; 68 | } 69 | label { 70 | padding: .1rem .25rem 0 0; 71 | vertical-align: middle; 72 | } 73 | input[type="number"] { 74 | width: 3.5em; 75 | text-align: left; 76 | padding-left: .25em; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/edit/style-settings.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 |
    13 | 16 | 19 | 22 |
    23 | 27 | 31 | 32 |
    33 | 34 |   35 | 38 |
    39 | -------------------------------------------------------------------------------- /src/edit/unload.js: -------------------------------------------------------------------------------- 1 | import {kCodeMirror} from '@/js/consts'; 2 | import {API} from '@/js/msg-api'; 3 | import * as prefs from '@/js/prefs'; 4 | import {sessionStore, t} from '@/js/util'; 5 | import editor from './editor'; 6 | import {helpPopup} from './util'; 7 | 8 | window.on('beforeunload', e => { 9 | let pos; 10 | if (editor.isWindowed && 11 | document.visibilityState === 'visible' && 12 | prefs.__values.openEditInWindow && 13 | screenX !== -32000 && // Chrome uses this value for minimized windows 14 | ( // only if not maximized 15 | screenX > 0 || outerWidth < screen.availWidth || 16 | screenY > 0 || outerHeight < screen.availHeight || 17 | screenX <= -10 || outerWidth >= screen.availWidth + 10 || 18 | screenY <= -10 || outerHeight >= screen.availHeight + 10 19 | ) 20 | ) { 21 | pos = { 22 | left: screenX, 23 | top: screenY, 24 | width: outerWidth, 25 | height: outerHeight, 26 | }; 27 | prefs.set('windowPosition', pos); 28 | } 29 | sessionStore.windowPos = JSON.stringify(pos || {}); 30 | API.saveScroll(editor.style.id, editor.makeScrollInfo()); 31 | const activeElement = document.activeElement; 32 | if (activeElement) { 33 | // blurring triggers 'change' or 'input' event if needed 34 | activeElement.blur(); 35 | // refocus if unloading was canceled 36 | setTimeout(() => activeElement.focus()); 37 | } 38 | if (editor.dirty.isDirty() || 39 | [].some.call(document.$$(helpPopup.SEL + ` .${kCodeMirror}`), el => 40 | !el[kCodeMirror].isClean())) { 41 | // neither confirm() nor custom messages work in modern browsers but just in case 42 | e.returnValue = t('styleChangesNotSaved'); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/edit/windowed-mode.js: -------------------------------------------------------------------------------- 1 | import '@/js/browser'; 2 | import * as prefs from '@/js/prefs'; 3 | import {FIREFOX} from '@/js/ua'; 4 | import {sessionStore, tryJSONparse} from '@/js/util'; 5 | import {browserWindows, getOwnTab} from '@/js/util-webext'; 6 | import editor from './editor'; 7 | import EmbeddedPopup from './embedded-popup'; 8 | 9 | let ownTabId; 10 | if (browserWindows) { 11 | initWindowedMode(); 12 | const pos = tryJSONparse(sessionStore.windowPos); 13 | delete sessionStore.windowPos; 14 | // resize the window on 'undo close' 15 | if (pos && pos.left != null) { 16 | browserWindows.update(browserWindows.WINDOW_ID_CURRENT, pos); 17 | } 18 | } 19 | 20 | getOwnTab().then(tab => { 21 | ownTabId = tab.id; 22 | if (sessionStore['manageStylesHistory' + ownTabId] === location.href) { 23 | editor.cancel = () => history.back(); 24 | } 25 | }); 26 | 27 | async function initWindowedMode() { 28 | chrome.tabs.onAttached.addListener(onTabAttached); 29 | // Chrome 96+ bug: the type is 'app' for a window that was restored via Ctrl-Shift-T 30 | const isSimple = ['app', 'popup'].includes((await browserWindows.getCurrent()).type); 31 | if (isSimple) EmbeddedPopup(); 32 | editor.isWindowed = isSimple || ( 33 | history.length === 1 && 34 | (__.MV3 || await prefs.ready, prefs.__values['openEditInWindow']) && 35 | (await browserWindows.getAll()).length > 1 && 36 | (await browser.tabs.query({currentWindow: true})).length === 1 37 | ); 38 | } 39 | 40 | async function onTabAttached(tabId, info) { 41 | if (tabId !== ownTabId) { 42 | return; 43 | } 44 | if (info.newPosition !== 0) { 45 | prefs.set('openEditInWindow', false); 46 | return; 47 | } 48 | const win = await browserWindows.get(info.newWindowId, {populate: true}); 49 | // If there's only one tab in this window, it's been dragged to new window 50 | const openEditInWindow = win.tabs.length === 1; 51 | // FF-only because Chrome retardedly resets the size during dragging 52 | if (openEditInWindow && FIREFOX) { 53 | browserWindows.update(info.newWindowId, prefs.__values['windowPosition']); 54 | } 55 | prefs.set('openEditInWindow', openEditInWindow); 56 | } 57 | -------------------------------------------------------------------------------- /src/icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/128.png -------------------------------------------------------------------------------- /src/icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/16.png -------------------------------------------------------------------------------- /src/icon/16w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/16w.png -------------------------------------------------------------------------------- /src/icon/16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/16x.png -------------------------------------------------------------------------------- /src/icon/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/19.png -------------------------------------------------------------------------------- /src/icon/19w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/19w.png -------------------------------------------------------------------------------- /src/icon/19x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/19x.png -------------------------------------------------------------------------------- /src/icon/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/32.png -------------------------------------------------------------------------------- /src/icon/32w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/32w.png -------------------------------------------------------------------------------- /src/icon/32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/32x.png -------------------------------------------------------------------------------- /src/icon/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/38.png -------------------------------------------------------------------------------- /src/icon/38w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/38w.png -------------------------------------------------------------------------------- /src/icon/38x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/38x.png -------------------------------------------------------------------------------- /src/icon/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/48.png -------------------------------------------------------------------------------- /src/icon/eyedropper/16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/eyedropper/16px.png -------------------------------------------------------------------------------- /src/icon/eyedropper/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/eyedropper/32px.png -------------------------------------------------------------------------------- /src/icon/eyedropper/README.md: -------------------------------------------------------------------------------- 1 | Author: DesignContest 2 | License: https://creativecommons.org/licenses/by/4.0/ 3 | Source: https://iconarchive.com/show/outline-icons-by-designcontest/Eyedropper-icon.html -------------------------------------------------------------------------------- /src/icon/light/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/16.png -------------------------------------------------------------------------------- /src/icon/light/16w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/16w.png -------------------------------------------------------------------------------- /src/icon/light/16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/16x.png -------------------------------------------------------------------------------- /src/icon/light/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/19.png -------------------------------------------------------------------------------- /src/icon/light/19w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/19w.png -------------------------------------------------------------------------------- /src/icon/light/19x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/19x.png -------------------------------------------------------------------------------- /src/icon/light/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/32.png -------------------------------------------------------------------------------- /src/icon/light/32w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/32w.png -------------------------------------------------------------------------------- /src/icon/light/32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/32x.png -------------------------------------------------------------------------------- /src/icon/light/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/38.png -------------------------------------------------------------------------------- /src/icon/light/38w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/38w.png -------------------------------------------------------------------------------- /src/icon/light/38x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstyles/stylus/f3250f58685dd1348b8e3750491965bc84b56d77/src/icon/light/38x.png -------------------------------------------------------------------------------- /src/icons/check1.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/check2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/config.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/external.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/install.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/log.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/reorder.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/select-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/sort-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/update-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/usercss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/icons/v.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/install-usercss.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Loading... 8 | 9 | 10 | <%= htmlWebpackPlugin.tags.headTags %> 11 | 12 | 15 | 16 | 17 | 64 |
    65 |
    66 |
    67 | <%= htmlWebpackPlugin.tags.bodyTags %> 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/install-usercss/direct-downloader.js: -------------------------------------------------------------------------------- 1 | import {API} from '@/js/msg-api'; 2 | import {fetchText} from '@/js/util'; 3 | import {CHROME} from '@/js/ua'; 4 | 5 | export default function DirectDownloader(url) { 6 | const opts = { 7 | // Disabling cache on http://localhost otherwise the recheck delay gets too big 8 | headers: {'Cache-Control': 'no-cache, no-store'}, 9 | }; 10 | let oldCode = null; 11 | return async () => { 12 | const code = CHROME < 99 // old Chrome can't fetch file:// 13 | ? await API.download(url, opts) 14 | : await fetchText(url, opts); 15 | if (oldCode !== code) { 16 | oldCode = code; 17 | return code; 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/install-usercss/port-downloader.js: -------------------------------------------------------------------------------- 1 | import {closeCurrentTab} from '@/js/util-webext'; 2 | 3 | export default function PortDownloader(url, tabId) { 4 | const resolvers = new Map(); 5 | const port = chrome.tabs.connect(tabId, {name: 'downloadSelf'}); 6 | port.onMessage.addListener(({id, code, error}) => { 7 | const r = resolvers.get(id); 8 | resolvers.delete(id); 9 | if (error) { 10 | r.reject(error); 11 | } else { 12 | r.resolve(code); 13 | } 14 | }); 15 | port.onDisconnect.addListener(async () => { 16 | const tab = await browser.tabs.get(tabId).catch(() => ({})); 17 | if (tab.url === url) { 18 | location.reload(); 19 | } else { 20 | closeCurrentTab(); 21 | } 22 | }); 23 | return (opts = {}) => new Promise((resolve, reject) => { 24 | const id = performance.now(); 25 | resolvers.set(id, {resolve, reject}); 26 | opts.id = id; 27 | port.postMessage(opts); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/js/chrome-sync.js: -------------------------------------------------------------------------------- 1 | import {compressToUTF16, decompressFromUTF16} from 'lz-string-unsafe'; 2 | import './browser'; 3 | import {sleep, tryJSONparse} from './util'; 4 | 5 | const syncApi = browser.storage.sync; 6 | const kMAX = 'MAX_WRITE_OPERATIONS_PER_MINUTE'; 7 | export const LZ_KEY = { 8 | csslint: 'editorCSSLintConfig', 9 | stylelint: 'editorStylelintConfig', 10 | usercssTemplate: 'usercssTemplate', 11 | }; 12 | /** @type {() => Promise} */ 13 | export const clear = /*@__PURE__*/run.bind(syncApi.clear); 14 | /** @type {(what: string | string[]) => Promise} */ 15 | export const remove = /*@__PURE__*/run.bind(syncApi.remove); 16 | /** @type {(what: string | string[] | object) => Promise} */ 17 | export const get = /*@__PURE__*/syncApi.get.bind(syncApi); 18 | /** @type {(what: object) => Promise} */ 19 | export const set = /*@__PURE__*/run.bind(syncApi.set); 20 | const toLZ = value => compressToUTF16(JSON.stringify(value)); 21 | export const unLZ = val => tryJSONparse(decompressFromUTF16(val)); 22 | export const getLZValue = async key => unLZ((await get(key))[key]); 23 | export const setLZValue = (key, value) => set({[key]: toLZ(value)}); 24 | 25 | let busy; 26 | 27 | export async function getLZValues(keys = Object.values(LZ_KEY)) { 28 | const data = await get(keys); 29 | for (const key of keys) { 30 | const value = data[key]; 31 | data[key] = value && unLZ(value); 32 | } 33 | return data; 34 | } 35 | 36 | export function setLZValues(data) { 37 | const res = {}; 38 | for (const key in data) res[key] = toLZ(data[key]); 39 | return set(res); 40 | } 41 | 42 | export async function run(...args) { 43 | while (true) { 44 | try { 45 | if (!busy) return await (busy = this.apply(syncApi, args)); 46 | await busy.catch(() => 0); 47 | } catch (err) { 48 | if (!err.message.includes(kMAX)) throw err; 49 | busy = sleep(60e3 / (syncApi[kMAX] || 120) * (Math.random() * 2 + 1)); 50 | await __.KEEP_ALIVE(busy); 51 | } finally { 52 | busy = null; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/js/cmpver.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copied from https://github.com/violentmonkey/violentmonkey/blob/master/src/common/util.js 3 | and switched to Math.sign 4 | */ 5 | 6 | const VERSION_RE = /^(.*?)-([-.0-9a-z]+)|$/i; 7 | const DIGITS_RE = /^\d+$/; // using regexp to avoid +'1e2' being parsed as 100 8 | 9 | /** @return -1 | 0 | 1 */ 10 | export default function compareVersion(ver1, ver2) { 11 | const [, main1 = ver1 || '', pre1] = VERSION_RE.exec(ver1); 12 | const [, main2 = ver2 || '', pre2] = VERSION_RE.exec(ver2); 13 | const delta = compareVersionChunk(main1, main2) 14 | || !pre1 - !pre2 // 1.2.3-pre-release is less than 1.2.3 15 | || pre1 && compareVersionChunk(pre1, pre2, true); // if pre1 is present, pre2 is too 16 | return Math.sign(delta || 0); 17 | } 18 | 19 | function compareVersionChunk(ver1, ver2, isSemverMode) { 20 | const parts1 = ver1.split('.'); 21 | const parts2 = ver2.split('.'); 22 | const len1 = parts1.length; 23 | const len2 = parts2.length; 24 | const len = (isSemverMode ? Math.min : Math.max)(len1, len2); 25 | let delta; 26 | for (let i = 0; !delta && i < len; i += 1) { 27 | const a = parts1[i]; 28 | const b = parts2[i]; 29 | if (isSemverMode) { 30 | delta = DIGITS_RE.test(a) && DIGITS_RE.test(b) 31 | ? a - b 32 | : a > b || a < b && -1; 33 | } else { 34 | delta = (parseInt(a, 10) || 0) - (parseInt(b, 10) || 0); 35 | } 36 | } 37 | return delta || isSemverMode && (len1 - len2); 38 | } 39 | -------------------------------------------------------------------------------- /src/js/color/LICENSE: -------------------------------------------------------------------------------- 1 | https://github.com/easylogic/codemirror-colorpicker 2 | 3 | https://github.com/easylogic/codemirror-colorpicker/blob/master/LICENSE 4 | 5 | 6 | MIT License 7 | 8 | Copyright (c) 2017 jinho park (cyberuls@gmail.com, easylogic) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /src/js/color/README.md: -------------------------------------------------------------------------------- 1 | ## color-picker - forked from v1.0.9 2 | 3 | codemirror-colorpicker was **heavily** modified from its source: 4 | 5 | https://github.com/easylogic/codemirror-colorpicker/... 6 | 7 | Shortly after this version the source repo split the file and built it using rollup. It should be considered a fork of the original. 8 | -------------------------------------------------------------------------------- /src/js/color/color-mimicry.js: -------------------------------------------------------------------------------- 1 | import {$create} from '@/js/dom'; 2 | import {debounce} from '@/js/util'; 3 | 4 | const styleCache = new Map(); 5 | 6 | /** 7 | * Calculates real color of an element: 8 | * colorMimicry(cm.display.gutters, {bg: 'backgroundColor'}) 9 | * colorMimicry('input.foo.bar', null, $('some.parent.to.host.the.dummy')) 10 | */ 11 | export default function colorMimicry(el, targets, dummyContainer = document.body) { 12 | targets = targets || {}; 13 | targets.fore = 'color'; 14 | const colors = {}; 15 | const done = {}; 16 | let numDone = 0; 17 | let numTotal = 0; 18 | const rootStyle = getStyle(document.documentElement); 19 | for (const k in targets) { 20 | const base = {r: 0, g: 0, b: 0, a: 0}; 21 | blend(base, rootStyle[targets[k]]); 22 | colors[k] = base; 23 | numTotal++; 24 | } 25 | const isDummy = typeof el === 'string'; 26 | if (isDummy) { 27 | el = dummyContainer.appendChild($create(el, {style: 'display: none'})); 28 | } 29 | for (let current = el; current; current = current && current.parentElement) { 30 | const style = getStyle(current); 31 | for (const k in targets) { 32 | if (!done[k]) { 33 | done[k] = blend(colors[k], style[targets[k]]); 34 | numDone += done[k] ? 1 : 0; 35 | if (numDone === numTotal) { 36 | current = null; 37 | break; 38 | } 39 | } 40 | } 41 | colors.style = colors.style || style; 42 | } 43 | if (isDummy) { 44 | el.remove(); 45 | } 46 | for (const k in targets) { 47 | const c = colors[k]; 48 | if (!isOpaque(c)) { 49 | blend(colors[k] = {r: 255, g: 255, b: 255, a: 1}, c); 50 | } 51 | const {r, g, b, a} = colors[k]; 52 | colors[k] = `rgba(${r}, ${g}, ${b}, ${a})`; 53 | // https://www.w3.org/TR/AERT#color-contrast 54 | colors[k + 'Luma'] = (r * .299 + g * .587 + b * .114) / 256; 55 | } 56 | debounce(clearCache); 57 | return colors; 58 | } 59 | 60 | function blend(base, color) { 61 | let r, g, b, a; 62 | if (typeof color === 'string') { 63 | [r, g, b, a = 255] = (color.match(/\d+/g) || []).map(Number); 64 | } else { 65 | ({r, g, b, a = 255} = color); 66 | } 67 | if (a === 255) { 68 | base.r = r; 69 | base.g = g; 70 | base.b = b; 71 | base.a = 1; 72 | } else if (a) { 73 | const mixedA = 1 - (1 - a / 255) * (1 - base.a); 74 | const q1 = a / 255 / mixedA; 75 | const q2 = base.a * (1 - mixedA) / mixedA; 76 | base.r = Math.round(r * q1 + base.r * q2); 77 | base.g = Math.round(g * q1 + base.g * q2); 78 | base.b = Math.round(b * q1 + base.b * q2); 79 | base.a = mixedA; 80 | } 81 | return isOpaque(base); 82 | } 83 | 84 | /** Speed-up for sequential invocations within the same event loop cycle 85 | * (we're assuming the invoker doesn't force CSSOM to refresh between the calls) */ 86 | function getStyle(el) { 87 | let style = styleCache.get(el); 88 | if (!style) { 89 | style = getComputedStyle(el); 90 | styleCache.set(el, style); 91 | } 92 | return style; 93 | } 94 | 95 | function clearCache() { 96 | styleCache.clear(); 97 | } 98 | 99 | function isOpaque({a}) { 100 | return Math.abs(a - 1) < 1e-3; 101 | } 102 | -------------------------------------------------------------------------------- /src/js/consts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING! 3 | * Used in limited contexts such as the offscreen document. 4 | * All values are unconditionally inlined via webpack-inline-constant-exports-plugin. 5 | * Only for pure declarations with no side effects or marked with /*@__PURE__*/ 6 | /** */// TODO: collect consts from the entire code 7 | 8 | export const UCD = 'usercssData'; 9 | export const kAboutBlank = 'about:blank'; 10 | export const kAppJson = 'application/json'; 11 | export const kAppUrlencoded = 'application/x-www-form-urlencoded'; 12 | export const kApplyPort = 'apply'; 13 | export const kCodeMirror = 'CodeMirror'; 14 | export const kContentType = 'content-type'; // must be lowercase! 15 | export const kCssPropSuffix = ': '; 16 | export const kDark = 'dark'; 17 | export const kDisableAll = 'disableAll'; 18 | export const kEditorSettings = 'editorSettings'; 19 | export const kHocused = 'focusedViaClick'; 20 | export const kHocusedAttr = 'data-focused-via-click'; 21 | export const kInjectionOrder = 'injectionOrder'; 22 | export const kInstall = 'install'; 23 | export const kInvokeAPI = 'invokeAPI'; 24 | export const kMainFrame = 'main_frame'; 25 | export const kPopup = 'popup'; 26 | export const kResolve = 'resolve'; 27 | export const kStyleIdPrefix = 'style-'; 28 | export const kStyleIds = 'styleIds'; 29 | export const kStyleViaXhr = 'styleViaXhr'; 30 | export const kSubFrame = 'sub_frame'; 31 | export const kUrl = 'url'; 32 | export const kUrls = 'urls'; 33 | export const k_busy = '_busy'; 34 | export const k_deepCopy = '_deepCopy'; 35 | export const k_msgExec = '_msgExec'; 36 | export const k_size = '_size'; 37 | 38 | export const CACHE_DB = 'cache'; 39 | export const DB = 'stylish'; 40 | export const STATE_DB = 'state'; 41 | 42 | export const IMPORT_THROTTLE = 100; //ms 43 | 44 | export const BIT_DARK = 1; 45 | export const BIT_SYS_DARK = 2; 46 | 47 | //#region prefs 48 | export const pKeepAlive = 'keepAlive'; 49 | //#endregion 50 | -------------------------------------------------------------------------------- /src/js/dnr.js: -------------------------------------------------------------------------------- 1 | export const DNR_ID_IDENTITY = 1e6; 2 | export const DNR_ID_INSTALLER = 1; 3 | 4 | export const DNR = __.MV3 && chrome.declarativeNetRequest; 5 | /** 6 | * @param {chrome.declarativeNetRequest.Rule[]} [addRules] 7 | * @param {number[]} [removeRuleIds] 8 | * @return {Promise} 9 | */ 10 | export const updateDynamicRules = __.MV3 && updateDNR.bind(DNR.updateDynamicRules); 11 | /** 12 | * @param {chrome.declarativeNetRequest.Rule[]} addRules 13 | * @param {number[]} [removeRuleIds] 14 | * @return {Promise} 15 | */ 16 | export const updateSessionRules = __.MV3 && updateDNR.bind(DNR.updateSessionRules); 17 | 18 | const getRuleId = r => r.id; 19 | export const getRuleIds = rules => rules.map(getRuleId); 20 | 21 | function updateDNR( 22 | addRules, 23 | removeRuleIds = getRuleIds(addRules), 24 | ) { 25 | return this({addRules, removeRuleIds}); 26 | } 27 | 28 | if (__.MV3 && (__.DEBUG || __.DEV)) { 29 | DNR.onRuleMatchedDebug?.addListener(console.log.bind(null, 'DNR')); 30 | } 31 | -------------------------------------------------------------------------------- /src/js/get-client-data.js: -------------------------------------------------------------------------------- 1 | import {isCssDarkScheme, makePropertyPopProxy} from './util'; 2 | 3 | self[__.CLIENT_DATA] = makePropertyPopProxy({}); 4 | document.write(``); 8 | -------------------------------------------------------------------------------- /src/js/header-resizer.js: -------------------------------------------------------------------------------- 1 | import {dom} from './dom'; 2 | import * as prefs from './prefs'; 3 | 4 | export default function HeaderResizer() { 5 | let curW = $id('header').offsetWidth; 6 | let offset, perPage; 7 | prefs.subscribe(dom.HWprefId, (key, val) => setWidth(val)); 8 | $id('header-resizer').onmousedown = e => { 9 | if (e.button) return; 10 | offset = curW - e.clientX; 11 | perPage = e.shiftKey; 12 | document.body.classList.add('resizing-h'); 13 | document.on('mousemove', resize); 14 | document.on('mouseup', resizeStop); 15 | }; 16 | 17 | function resize(e) { 18 | setWidth(offset + e.clientX); 19 | } 20 | 21 | function resizeStop() { 22 | document.off('mouseup', resizeStop); 23 | document.off('mousemove', resize); 24 | document.body.classList.remove('resizing-h'); 25 | save(); 26 | } 27 | 28 | function save() { 29 | if (perPage) { 30 | prefs.set(dom.HWprefId, curW); 31 | } else { 32 | for (const k of prefs.knownKeys) { 33 | if (k.startsWith(dom.HW)) prefs.set(k, curW); 34 | } 35 | } 36 | } 37 | 38 | function setWidth(w) { 39 | const delta = (w = dom.setHWProp(w)) - curW; 40 | if (delta) { 41 | curW = w; 42 | for (const el of $$('.CodeMirror-linewidget[style*="width:"]')) { 43 | el.style.width = parseFloat(el.style.width) - delta + 'px'; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/js/meta-parser.js: -------------------------------------------------------------------------------- 1 | import {createParser, ParseError} from 'usercss-meta'; 2 | import {importScriptsOnce} from './worker-util'; 3 | 4 | const PREPROCESSORS = new Set(['default', 'uso', 'stylus', 'less']); 5 | const options = { 6 | validateKey: { 7 | preprocessor: state => { 8 | if (!PREPROCESSORS.has(state.value)) { 9 | throw new ParseError({ 10 | code: 'unknownPreprocessor', 11 | args: [state.value], 12 | index: state.valueIndex, 13 | }); 14 | } 15 | }, 16 | }, 17 | validateVar: { 18 | select: state => { 19 | if (state.varResult.options.every(o => o.name !== state.value)) { 20 | throw new ParseError({ 21 | code: 'invalidSelectValueMismatch', 22 | index: state.valueIndex, 23 | }); 24 | } 25 | }, 26 | color: state => { 27 | importScriptsOnce('color-converter.js'); /* global colorConverter */ 28 | const color = colorConverter.parse(state.value); 29 | if (!color) { 30 | throw new ParseError({ 31 | code: 'invalidColor', 32 | args: [state.value], 33 | index: state.valueIndex, 34 | }); 35 | } 36 | state.value = colorConverter.format(color); 37 | }, 38 | }, 39 | }; 40 | const parser = createParser(options); 41 | const looseParser = createParser(Object.assign({}, options, { 42 | allowErrors: true, 43 | unknownKey: 'throw', 44 | })); 45 | 46 | const metaParser = { 47 | 48 | lint: looseParser.parse, 49 | parse: parser.parse, 50 | 51 | nullifyInvalidVars(vars) { 52 | for (const va of Object.values(vars)) { 53 | if (va.value !== null) { 54 | try { 55 | parser.validateVar(va); 56 | } catch { 57 | va.value = null; 58 | } 59 | } 60 | } 61 | return vars; 62 | }, 63 | }; 64 | 65 | export default metaParser; 66 | -------------------------------------------------------------------------------- /src/js/msg-api.js: -------------------------------------------------------------------------------- 1 | import {kInvokeAPI} from '@/js/consts'; 2 | 3 | export const FF = __.BUILD !== 'chrome' && ( 4 | __.ENTRY 5 | ? 'contextualIdentities' in chrome || 'activityLog' in chrome 6 | : global !== window 7 | ); 8 | export const rxIgnorableError = /(R)eceiving end does not exist|The message (port|channel) closed|moved into back\/forward cache/; 9 | 10 | export const apiHandler = !__.IS_BG && { 11 | get: ({name: path}, name) => new Proxy( 12 | Object.defineProperty(() => {}, 'name', {value: path ? path + '.' + name : name}), 13 | apiHandler), 14 | apply: apiSendProxy, 15 | }; 16 | /** @typedef {{}} API */ 17 | /** @type {API} */ 18 | export const API = __.IS_BG 19 | ? global[__.API] 20 | : global[__.API] = new Proxy({path: ''}, apiHandler); 21 | export const isFrame = !__.IS_BG && window !== top; 22 | 23 | export let bgReadySignal; 24 | let bgReadying = !__.MV3 && new Promise(fn => (bgReadySignal = fn)); 25 | /** @type {number} top document mode 26 | * -1 = top prerendered, 0 = iframe, 1 = top, 2 = top reified */ 27 | export let TDM = isFrame ? 0 : !__.IS_BG && document.prerendering ? -1 : 1; 28 | 29 | export function updateTDM(value) { 30 | TDM = value; 31 | } 32 | 33 | export async function apiSendProxy({name: path}, thisObj, args) { 34 | const localErr = new Error(); 35 | const msg = {data: {method: kInvokeAPI, path, args}, TDM}; 36 | for (let res, err, retry = 0; retry < 2; retry++) { 37 | try { 38 | if (__.MV3 || FF) { 39 | res = await (FF ? browser : chrome).runtime.sendMessage(msg); 40 | } else { 41 | res = await new Promise((resolve, reject) => 42 | chrome.runtime.sendMessage(msg, res2 => 43 | ((err = chrome.runtime.lastError)) ? reject(err) : resolve(res2))); 44 | } 45 | if (res) { 46 | bgReadying = bgReadySignal = null; 47 | if ((err = res.error)) { 48 | err.stack += '\n' + localErr.stack; 49 | throw err; 50 | } else { 51 | return res.data; 52 | } 53 | } 54 | } catch (e) { 55 | if (!bgReadying) { 56 | e.stack = localErr.stack; 57 | throw e; 58 | } 59 | } 60 | if (retry) { 61 | throw new Error('Stylus could not connect to the background script.'); 62 | } 63 | await bgReadying; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/js/msg-init.js: -------------------------------------------------------------------------------- 1 | /** Don't use this file in content script context! */ 2 | import './browser'; 3 | import {k_busy, k_deepCopy, k_msgExec, kInvokeAPI} from '@/js/consts'; 4 | import {apiHandler, apiSendProxy} from './msg-api'; 5 | import {createPortExec, createPortProxy} from './port'; 6 | import {swPath, workerPath} from './urls'; 7 | import {deepCopy} from './util'; 8 | import {getOwnTab} from './util-webext'; 9 | 10 | const needsTab = [ 11 | 'updateIconBadge', 12 | 'styleViaAPI', 13 | ]; 14 | /** @type {MessagePort} */ 15 | const swExec = __.MV3 && 16 | createPortExec(() => navigator.serviceWorker.controller, {lock: swPath}); 17 | const workerApiPrefix = 'worker.'; 18 | let workerProxy; 19 | export let bg = __.IS_BG ? self : !__.MV3 && chrome.extension.getBackgroundPage(); 20 | 21 | async function invokeAPI({name: path}, _thisObj, args) { 22 | // Non-cloneable event is passed when doing `elem.onclick = API.foo` 23 | if (args[0] instanceof Event) args[0] = 'Event'; 24 | if (path.startsWith(workerApiPrefix)) { 25 | workerProxy ??= createPortProxy(workerPath); 26 | return workerProxy[path.slice(workerApiPrefix.length)](...args); 27 | } 28 | let tab = false; 29 | // Using a fake id for our Options frame as we want to fetch styles early 30 | const frameId = window === top ? 0 : 1; 31 | if (!needsTab.includes(path) || !frameId && (tab = await getOwnTab())) { 32 | const msg = {method: kInvokeAPI, path, args}; 33 | const sender = {url: location.href, tab, frameId}; 34 | if (__.MV3) { 35 | return swExec(msg, sender); 36 | } else { 37 | const bgDeepCopy = bg[k_deepCopy]; 38 | const res = bg[k_msgExec](bgDeepCopy(msg), bgDeepCopy(sender)); 39 | return deepCopy(await res); 40 | } 41 | } 42 | } 43 | 44 | if (__.MV3) { 45 | if (__.ENTRY !== 'sw') { 46 | apiHandler.apply = invokeAPI; 47 | } 48 | } else if (!__.IS_BG) { 49 | apiHandler.apply = async (fn, thisObj, args) => { 50 | bg ??= await browser.runtime.getBackgroundPage().catch(() => {}) || false; 51 | const exec = bg && (bg[k_msgExec] || await bg[k_busy]) 52 | ? invokeAPI 53 | : apiSendProxy; 54 | return exec(fn, thisObj, args); 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/js/msg.js: -------------------------------------------------------------------------------- 1 | import {k_busy, kInvokeAPI} from '@/js/consts'; 2 | import {bgReadySignal} from './msg-api'; 3 | 4 | /** @type {Map} true: returned value is used as the reply */ 5 | export const onMessage = new Map(); 6 | export const onConnect = {}; 7 | export const onDisconnect = {}; 8 | export const wrapData = data => ({ 9 | data, 10 | }); 11 | export const wrapError = error => ({ 12 | error: Object.assign({ 13 | message: error.message || `${error}`, 14 | stack: error.stack, 15 | }, error), // passing custom properties e.g. `error.index` 16 | }); 17 | 18 | chrome.runtime.onMessage.addListener(onRuntimeMessage); 19 | if (__.ENTRY) { 20 | chrome.runtime.onConnect.addListener(async port => { 21 | if (__.IS_BG && global[k_busy]) await global[k_busy]; 22 | const name = port.name.split(':', 1)[0]; 23 | const fnOn = onConnect[name]; 24 | const fnOff = onDisconnect[name]; 25 | if (fnOn) fnOn(port); 26 | if (fnOff) port.onDisconnect.addListener(fnOff); 27 | }); 28 | } 29 | 30 | export function _execute(data, sender, multi) { 31 | let result; 32 | let res; 33 | let i = 0; 34 | if (__.ENTRY !== 'sw' && multi) { 35 | multi = data.length > 1 && data; 36 | data = data[0]; 37 | } 38 | do { 39 | for (const [fn, replyAllowed] of onMessage) { 40 | try { 41 | res = fn(data, sender, !!multi); 42 | } catch (err) { 43 | res = Promise.reject(err); 44 | } 45 | if (replyAllowed && res !== result && result === undefined) { 46 | result = res; 47 | } 48 | } 49 | } while (__.ENTRY !== 'sw' && multi && (data = multi[++i])); 50 | return result; 51 | } 52 | 53 | function onRuntimeMessage({data, multi, TDM}, sender, sendResponse) { 54 | if (!__.MV3 && !__.IS_BG && data.method === 'backgroundReady') { 55 | bgReadySignal?.(true); 56 | } 57 | if (__.ENTRY === true && !__.IS_BG && data.method === kInvokeAPI) { 58 | return; 59 | } 60 | sender.TDM = TDM; 61 | let res = __.IS_BG && global[k_busy]; 62 | res = res 63 | ? res.then(_execute.bind(null, data, sender, multi)) 64 | : _execute(data, sender, multi); 65 | if (res instanceof Promise) { 66 | res.then(wrapData, wrapError).then(sendResponse); 67 | return true; 68 | } 69 | if (res !== undefined) sendResponse(wrapData(res)); 70 | } 71 | -------------------------------------------------------------------------------- /src/js/storage-util.js: -------------------------------------------------------------------------------- 1 | import './browser'; 2 | 3 | const StorageExtras = { 4 | async getValue(key) { 5 | return (await this.get(key))[key]; 6 | }, 7 | }; 8 | 9 | export const chromeLocal = 10 | /*@__PURE__*/Object.assign(browser.storage.local, StorageExtras); 11 | export const chromeSession = browser.storage.session; 12 | export const GET_KEYS = !!chromeLocal.getKeys; 13 | -------------------------------------------------------------------------------- /src/js/sync-util.js: -------------------------------------------------------------------------------- 1 | import {capitalize, t} from './util'; 2 | 3 | export const connected = 'connected'; 4 | export const connecting = 'connecting'; 5 | export const disconnected = 'disconnected'; 6 | export const disconnecting = 'disconnecting'; 7 | export const pending = 'pending'; 8 | 9 | export const DRIVE_NAMES = { 10 | dropbox: 'Dropbox', 11 | google: 'Google Drive', 12 | onedrive: 'OneDrive', 13 | webdav: 'WebDAV', 14 | }; 15 | 16 | const getPhaseText = (phase, loaded, total) => 17 | t(`optionsSyncStatus${capitalize(phase)}`, total && [loaded + 1, total], false); 18 | 19 | export const getStatusText = (status, verbose) => { 20 | if (status.syncing) { 21 | const {phase, loaded, total} = status.progress || {}; 22 | return phase 23 | ? getPhaseText(phase, loaded, total) || `${phase} ${loaded} / ${total}` 24 | : t('optionsSyncStatusSyncing'); 25 | } 26 | const {state, errorMessage} = status; 27 | if (errorMessage && (state === connected || state === disconnected)) { 28 | return errorMessage; 29 | } 30 | if (state === connected && !status.login) { 31 | return t('optionsSyncStatusRelogin'); 32 | } 33 | return verbose && getPhaseText(state) || state; 34 | }; 35 | -------------------------------------------------------------------------------- /src/js/themer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file must be loaded in a