Compare commits
3 Commits
master
...
insertcss2
Author | SHA1 | Date | |
---|---|---|---|
|
7d094847f6 | ||
|
62053316a2 | ||
|
aff4707bf0 |
|
@ -1,2 +1,3 @@
|
||||||
vendor/
|
vendor/
|
||||||
vendor-overwrites/
|
vendor-overwrites/*
|
||||||
|
!vendor-overwrites/colorpicker
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
|
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
|
||||||
|
|
||||||
parserOptions:
|
parserOptions:
|
||||||
ecmaVersion: 2017
|
ecmaVersion: 2015
|
||||||
|
|
||||||
env:
|
env:
|
||||||
browser: true
|
browser: true
|
||||||
|
@ -9,7 +9,59 @@ env:
|
||||||
webextensions: true
|
webextensions: true
|
||||||
|
|
||||||
globals:
|
globals:
|
||||||
require: readonly # in polyfill.js
|
# messaging.js
|
||||||
|
KEEP_CHANNEL_OPEN: false
|
||||||
|
CHROME: false
|
||||||
|
FIREFOX: false
|
||||||
|
OPERA: false
|
||||||
|
URLS: false
|
||||||
|
BG: false
|
||||||
|
notifyAllTabs: false
|
||||||
|
sendMessage: false
|
||||||
|
queryTabs: false
|
||||||
|
getTab: false
|
||||||
|
getOwnTab: false
|
||||||
|
getActiveTab: false
|
||||||
|
getActiveTabRealURL: false
|
||||||
|
getTabRealURL: false
|
||||||
|
openURL: false
|
||||||
|
activateTab: false
|
||||||
|
stringAsRegExp: false
|
||||||
|
ignoreChromeError: false
|
||||||
|
tryCatch: false
|
||||||
|
tryRegExp: false
|
||||||
|
tryJSONparse: false
|
||||||
|
debounce: false
|
||||||
|
deepCopy: false
|
||||||
|
onBackgroundReady: false
|
||||||
|
deleteStyleSafe: false
|
||||||
|
getStylesSafe: false
|
||||||
|
saveStyleSafe: false
|
||||||
|
sessionStorageHash: false
|
||||||
|
download: false
|
||||||
|
invokeOrPostpone: false
|
||||||
|
# localization.js
|
||||||
|
template: false
|
||||||
|
t: false
|
||||||
|
o: false
|
||||||
|
tE: false
|
||||||
|
tHTML: false
|
||||||
|
tNodeList: false
|
||||||
|
tDocLoader: false
|
||||||
|
tWordBreak: false
|
||||||
|
# dom.js
|
||||||
|
onDOMready: false
|
||||||
|
onDOMscriptReady: false
|
||||||
|
scrollElementIntoView: false
|
||||||
|
enforceInputRange: false
|
||||||
|
animateElement: false
|
||||||
|
$: false
|
||||||
|
$$: false
|
||||||
|
$create: false
|
||||||
|
$createLink: false
|
||||||
|
# prefs.js
|
||||||
|
prefs: false
|
||||||
|
setupLivePrefs: false
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
accessor-pairs: [2]
|
accessor-pairs: [2]
|
||||||
|
@ -22,19 +74,19 @@ rules:
|
||||||
brace-style: [2, 1tbs, {allowSingleLine: false}]
|
brace-style: [2, 1tbs, {allowSingleLine: false}]
|
||||||
camelcase: [2, {properties: never}]
|
camelcase: [2, {properties: never}]
|
||||||
class-methods-use-this: [2]
|
class-methods-use-this: [2]
|
||||||
comma-dangle: [2, {arrays: always-multiline, objects: always-multiline}]
|
comma-dangle: [0]
|
||||||
comma-spacing: [2, {before: false, after: true}]
|
comma-spacing: [2, {before: false, after: true}]
|
||||||
comma-style: [2, last]
|
comma-style: [2, last]
|
||||||
complexity: [0]
|
complexity: [0]
|
||||||
computed-property-spacing: [2, never]
|
computed-property-spacing: [2, never]
|
||||||
consistent-return: [0]
|
consistent-return: [0]
|
||||||
constructor-super: [2]
|
constructor-super: [2]
|
||||||
curly: [2, "multi-line"]
|
curly: [2]
|
||||||
default-case: [0]
|
default-case: [0]
|
||||||
dot-location: [2, property]
|
dot-location: [2, property]
|
||||||
dot-notation: [0]
|
dot-notation: [0]
|
||||||
eol-last: [2]
|
eol-last: [2]
|
||||||
eqeqeq: [1, smart]
|
eqeqeq: [1, always]
|
||||||
func-call-spacing: [2, never]
|
func-call-spacing: [2, never]
|
||||||
func-name-matching: [0]
|
func-name-matching: [0]
|
||||||
func-names: [0]
|
func-names: [0]
|
||||||
|
@ -45,15 +97,7 @@ rules:
|
||||||
id-blacklist: [0]
|
id-blacklist: [0]
|
||||||
id-length: [0]
|
id-length: [0]
|
||||||
id-match: [0]
|
id-match: [0]
|
||||||
indent: [2, 2, {
|
indent-legacy: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
|
||||||
SwitchCase: 1,
|
|
||||||
ignoreComments: true,
|
|
||||||
ignoredNodes: [
|
|
||||||
"TemplateLiteral > *",
|
|
||||||
"ConditionalExpression",
|
|
||||||
"ForStatement"
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
jsx-quotes: [0]
|
jsx-quotes: [0]
|
||||||
key-spacing: [0]
|
key-spacing: [0]
|
||||||
keyword-spacing: [2]
|
keyword-spacing: [2]
|
||||||
|
@ -77,11 +121,11 @@ rules:
|
||||||
no-case-declarations: [2]
|
no-case-declarations: [2]
|
||||||
no-class-assign: [2]
|
no-class-assign: [2]
|
||||||
no-cond-assign: [2, except-parens]
|
no-cond-assign: [2, except-parens]
|
||||||
no-confusing-arrow: [0, {allowParens: true}]
|
no-confusing-arrow: [1, {allowParens: true}]
|
||||||
no-const-assign: [2]
|
no-const-assign: [2]
|
||||||
no-constant-condition: [0]
|
no-constant-condition: [0]
|
||||||
no-continue: [0]
|
no-continue: [0]
|
||||||
no-control-regex: [0]
|
no-control-regex: [2]
|
||||||
no-debugger: [2]
|
no-debugger: [2]
|
||||||
no-delete-var: [2]
|
no-delete-var: [2]
|
||||||
no-div-regex: [0]
|
no-div-regex: [0]
|
||||||
|
@ -95,9 +139,9 @@ rules:
|
||||||
no-empty-function: [0]
|
no-empty-function: [0]
|
||||||
no-empty-pattern: [2]
|
no-empty-pattern: [2]
|
||||||
no-empty: [2, {allowEmptyCatch: true}]
|
no-empty: [2, {allowEmptyCatch: true}]
|
||||||
no-eq-null: [0]
|
no-eq-null: [2]
|
||||||
no-eval: [2]
|
no-eval: [2]
|
||||||
no-ex-assign: [0]
|
no-ex-assign: [2]
|
||||||
no-extend-native: [2]
|
no-extend-native: [2]
|
||||||
no-extra-bind: [2]
|
no-extra-bind: [2]
|
||||||
no-extra-boolean-cast: [2]
|
no-extra-boolean-cast: [2]
|
||||||
|
@ -147,9 +191,6 @@ rules:
|
||||||
no-proto: [2]
|
no-proto: [2]
|
||||||
no-redeclare: [2]
|
no-redeclare: [2]
|
||||||
no-regex-spaces: [2]
|
no-regex-spaces: [2]
|
||||||
no-restricted-globals: [2, name, event]
|
|
||||||
# `name` and `event` (in Chrome) are built-in globals
|
|
||||||
# but we don't use these globals so it's most likely a mistake/typo
|
|
||||||
no-restricted-imports: [0]
|
no-restricted-imports: [0]
|
||||||
no-restricted-modules: [2, domain, freelist, smalloc, sys]
|
no-restricted-modules: [2, domain, freelist, smalloc, sys]
|
||||||
no-restricted-syntax: [2, WithStatement]
|
no-restricted-syntax: [2, WithStatement]
|
||||||
|
@ -170,16 +211,17 @@ rules:
|
||||||
no-trailing-spaces: [2]
|
no-trailing-spaces: [2]
|
||||||
no-undef-init: [2]
|
no-undef-init: [2]
|
||||||
no-undef: [2]
|
no-undef: [2]
|
||||||
|
no-undefined: [0]
|
||||||
no-underscore-dangle: [0]
|
no-underscore-dangle: [0]
|
||||||
no-unexpected-multiline: [2]
|
no-unexpected-multiline: [2]
|
||||||
no-unmodified-loop-condition: [0]
|
no-unmodified-loop-condition: [1]
|
||||||
no-unneeded-ternary: [2]
|
no-unneeded-ternary: [2]
|
||||||
no-unreachable: [2]
|
no-unreachable: [2]
|
||||||
no-unsafe-finally: [2]
|
no-unsafe-finally: [2]
|
||||||
no-unsafe-negation: [2]
|
no-unsafe-negation: [2]
|
||||||
no-unused-expressions: [2]
|
no-unused-expressions: [1]
|
||||||
no-unused-labels: [0]
|
no-unused-labels: [0]
|
||||||
no-unused-vars: [2, {args: after-used}]
|
no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}]
|
||||||
no-use-before-define: [2, nofunc]
|
no-use-before-define: [2, nofunc]
|
||||||
no-useless-call: [2]
|
no-useless-call: [2]
|
||||||
no-useless-computed-key: [2]
|
no-useless-computed-key: [2]
|
||||||
|
@ -194,16 +236,16 @@ rules:
|
||||||
object-curly-spacing: [2, never]
|
object-curly-spacing: [2, never]
|
||||||
object-shorthand: [0]
|
object-shorthand: [0]
|
||||||
one-var-declaration-per-line: [1]
|
one-var-declaration-per-line: [1]
|
||||||
one-var: [2, {initialized: never}]
|
one-var: [0]
|
||||||
operator-assignment: [2, always]
|
operator-assignment: [2, always]
|
||||||
operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
|
operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
|
||||||
padded-blocks: [0]
|
padded-blocks: [0]
|
||||||
prefer-numeric-literals: [2]
|
prefer-numeric-literals: [2]
|
||||||
prefer-rest-params: [0]
|
prefer-rest-params: [0]
|
||||||
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
|
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}]
|
||||||
quote-props: [0]
|
quote-props: [0]
|
||||||
quotes: [1, single, avoid-escape]
|
quotes: [1, single, avoid-escape]
|
||||||
radix: [2, always]
|
radix: [2, as-needed]
|
||||||
require-jsdoc: [0]
|
require-jsdoc: [0]
|
||||||
require-yield: [2]
|
require-yield: [2]
|
||||||
semi-spacing: [2, {before: false, after: true}]
|
semi-spacing: [2, {before: false, after: true}]
|
||||||
|
@ -225,16 +267,3 @@ rules:
|
||||||
wrap-iife: [2, inside]
|
wrap-iife: [2, inside]
|
||||||
yield-star-spacing: [2, {before: true, after: false}]
|
yield-star-spacing: [2, {before: true, after: false}]
|
||||||
yoda: [2, never]
|
yoda: [2, never]
|
||||||
|
|
||||||
overrides:
|
|
||||||
- files: [tools/*]
|
|
||||||
env:
|
|
||||||
node: true
|
|
||||||
browser: false
|
|
||||||
webextensions: false
|
|
||||||
parserOptions:
|
|
||||||
ecmaVersion: 2017
|
|
||||||
|
|
||||||
- files: ["**/*worker*.js"]
|
|
||||||
env:
|
|
||||||
worker: true
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
43
.github/CONTRIBUTING.md
vendored
43
.github/CONTRIBUTING.md
vendored
|
@ -1,46 +1,33 @@
|
||||||
# Contributing to Stylus
|
# Contributing to Stylus
|
||||||
|
|
||||||
1. [Getting involved](#getting-involved)
|
1. [Getting Involved](#getting-involved)
|
||||||
2. [How to report issues](#how-to-report-issues)
|
2. [How to Report Issues](#how-to-report-issues)
|
||||||
3. [Adding translations](#adding-translations)
|
3. [Adding Tranlations](#adding-translations)
|
||||||
4. [Pull requests](#pull-requests)
|
4. [Core Style Guide](#core-style-guide)
|
||||||
5. [Scripts](#scripts)
|
5. [Getting Started](#getting-started)
|
||||||
6. [Updating locale files](#updating-locale-files-admin-only)
|
|
||||||
7. [Contact us](#contact-us)
|
|
||||||
|
|
||||||
## Getting involved
|
## Getting Involved
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## How to report issues
|
## How to Report issues
|
||||||
|
|
||||||
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.
|
When an issue 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 direction on how to reproduce the issue as well as the browser, operating system and versions of each.
|
||||||
|
|
||||||
When adding a **feature request**, please search through the existing issues to see if it the feature has already been requested, added or rejected.
|
When adding a feature request, please include
|
||||||
|
|
||||||
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.
|
## Adding Translations
|
||||||
|
|
||||||
|
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/openstyles/stylus/issues).
|
||||||
|
|
||||||
## Adding translations
|
## Core Style Guide
|
||||||
|
|
||||||
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus).
|
* Use the provided `.editorconfig` file with your code editor. Don't know what that is? Then check out http://editorconfig.org/.
|
||||||
Only the languages supported by the web store are allowed:
|
|
||||||
https://developer.chrome.com/docs/webstore/i18n/#localeTable
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
## Pull requests
|
|
||||||
|
|
||||||
* First open an issue to discuss your changes.
|
* First open an issue to discuss your changes.
|
||||||
* Then download, fork or clone this repository.
|
* Then download, fork or clone this repository.
|
||||||
<!-- * Use [node.js](https://nodejs.org/) to run `npm install`. -->
|
<!-- * Use [node.js](http://nodejs.org/) to run `npm install`. -->
|
||||||
* Use the provided `.editorconfig` file with your code editor. Don't know what that is? Then check out https://editorconfig.org/.
|
|
||||||
* Make any changes within a branch of this repository (not the `master` branch).
|
* Make any changes within a branch of this repository (not the `master` branch).
|
||||||
* Submit a pull request and include a reference to the initial issue with the discussion.
|
* Submit a pull request and include a reference to the initial issue with the discussion.
|
||||||
|
|
||||||
## Build scripts
|
|
||||||
|
|
||||||
See [BUILD.md](../BUILD.md) for more information.
|
|
||||||
|
|
||||||
## Contact us
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
8
.github/ISSUE_TEMPLATE.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
* **Browser**:
|
||||||
|
* **Operating System**:
|
||||||
|
* **Screenshot**:
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please make sure you checked that your issue wasn't already addressed.
|
||||||
|
If the issue persists, please help us identifying the cause by providing the above details.
|
||||||
|
-->
|
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,48 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Create a report about a bug you experienced while using Stylus.
|
|
||||||
title: "[Bug] Replace with title"
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
⚠⚠ Do not delete this issue template! ⚠⚠
|
|
||||||
Reported issues must use this template and have all the necessary information provided.
|
|
||||||
Incomplete reports are likely to be ignored and closed.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for taking the time to create a report about a bug.
|
|
||||||
Ensure that there are no other existing reports for this bug.
|
|
||||||
Please check if the issue is resolved after a restart of the browser.
|
|
||||||
Additionally, you should check if the issue persists in a new browser profile.
|
|
||||||
Remember to fill out every section on this report and remove any that are not needed.
|
|
||||||
Finally, place a brief description in the title of this report.
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Bug Report
|
|
||||||
|
|
||||||
### Bug Description
|
|
||||||
<!-- Provide a clear and concise description, which will allow us to properly troubleshoot this bug. -->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
<!-- If applicable, add screenshots to help explain this bug. -->
|
|
||||||
|
|
||||||
### CSS Code
|
|
||||||
<!--
|
|
||||||
If the bug is related to (user)CSS or the editor,
|
|
||||||
please post the code (with a service like pastebin) in this bug report.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### System Information
|
|
||||||
<!--
|
|
||||||
Specify the browser name and version as well as the Stylus version you are using.
|
|
||||||
Please do an online search for help if you are not familiar with how to get this information.
|
|
||||||
-->
|
|
||||||
|
|
||||||
- OS: <!-- e.g. Windows, macOS, Linux -->
|
|
||||||
- Browser: <!-- e.g. Chrome 91, Firefox 90, Edge 91, Safari 14 -->
|
|
||||||
- Stylus Version: <!-- e.g. 1.5.21 -->
|
|
||||||
|
|
||||||
### Additional Context
|
|
||||||
<!-- Provide any additional information about this bug. -->
|
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -1,14 +0,0 @@
|
||||||
name: ci
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
- run: npm install
|
|
||||||
- run: npm test
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,8 +1,2 @@
|
||||||
*.zip
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
pull_locales_login.rb
|
||||||
.transifexrc
|
|
||||||
.vscode
|
|
||||||
desktop.ini
|
|
||||||
node_modules/
|
|
||||||
yarn.lock
|
|
||||||
|
|
10
.tx/config
10
.tx/config
|
@ -1,10 +0,0 @@
|
||||||
[main]
|
|
||||||
host = https://www.transifex.com
|
|
||||||
|
|
||||||
[Stylus.messages]
|
|
||||||
file_filter = _locales/<lang>/messages.json
|
|
||||||
minimum_perc = 0
|
|
||||||
source_file = _locales/en/messages.json
|
|
||||||
source_lang = en_US
|
|
||||||
type = CHROME
|
|
||||||
|
|
70
BUILD.md
70
BUILD.md
|
@ -1,70 +0,0 @@
|
||||||
# Build this project
|
|
||||||
|
|
||||||
## Preparation
|
|
||||||
|
|
||||||
1. Install [Node.js](https://nodejs.org/en/).
|
|
||||||
2. Go to the project root, run `npm install`. This will install all required dependencies.
|
|
||||||
|
|
||||||
Extra preparations are needed if you want to pull locale files from Transifex:
|
|
||||||
|
|
||||||
1. Install Transifex client. Follow the instructions on [this page](https://docs.transifex.com/client/installing-the-client).
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Generate the ZIP release
|
|
||||||
|
|
||||||
Use the following command to generate a ZIP file that can be submitted to AMO or CWS:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run zip
|
|
||||||
```
|
|
||||||
|
|
||||||
The zip file includes all the files from the repository **except**:
|
|
||||||
|
|
||||||
* All dot files (e.g. `.eslintrc` & `.gitignore`).
|
|
||||||
* `node_modules` folder.
|
|
||||||
* `tools` folder.
|
|
||||||
* `package.json` file.
|
|
||||||
* `package-lock.json` and/or `yarn.lock` file(s).
|
|
||||||
|
|
||||||
<!-- FIXME: is this statement still true?
|
|
||||||
* `vendor/codemirror/lib` files. This path is excluded because it contains a file modified for development purposes only. Instead, the CodeMirror files are copied directly from `node_modules/codemirror/lib`.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Tag a release/Bump the version
|
|
||||||
|
|
||||||
Use the `npm version (major | minor | patch)` command to tag a release.
|
|
||||||
|
|
||||||
There are some scripts that will run automatically before/after tagging a version. Includes:
|
|
||||||
|
|
||||||
1. Test.
|
|
||||||
2. Update version number in `manifest.json`.
|
|
||||||
3. Generate the ZIP file.
|
|
||||||
4. Push the tag to github.
|
|
||||||
|
|
||||||
## Translation
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
To pull files from Transifex, run
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run update-locales
|
|
||||||
```
|
|
||||||
|
|
||||||
To push files to Transifex:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run update-transifex
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3rd-party libraries
|
|
||||||
|
|
||||||
3rd-party libraries are managed by `npm`. Since Stylus is built with vanilla JS, we only use libraries that can run in the browser.
|
|
||||||
|
|
||||||
We keep a copy of these libraries inside the `vendor` directory so users can side-load this repository without executing the build script. These files are downloaded from CDN or pulled from npm (`node_modules`).
|
|
||||||
|
|
||||||
To add/update a library to the latest version, run `npm install PACKAGE_NAME@latest`.
|
|
||||||
|
|
||||||
To remove a library, run `npm uninstall PACKAGE_NAME`.
|
|
||||||
|
|
||||||
After the (un)installation, specify files which should be copied in `tools/build-vendor.js` and run `npm run build-vendor` to rebuild the vendor folder.
|
|
49
README.md
49
README.md
|
@ -1,35 +1,20 @@
|
||||||
# Stylus
|
|
||||||
|
|
||||||
Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExtension
|
Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExtension
|
||||||
|
|
||||||
## Highlights
|
|
||||||
|
|
||||||
* In addition to the userstyles.org site, styles with customizable parameters can also be installed from .user.css or .user.styl URLs (see [Usercss format wiki](https://github.com/openstyles/stylus/wiki/Usercss)).
|
|
||||||
* Site styles can be discovered and previewed in the popup using inline search with screenshot thumbnails.
|
|
||||||
* A backup feature which is compatible with other userstyles managers.
|
|
||||||
* Configurable automatic update function for installed styles.
|
|
||||||
* Customizable UI, optional layouts, and tweaks.
|
|
||||||
* Two different optional code validators with user-configurable rules: CSSLint and Stylelint.
|
|
||||||
* Both validators use Web Worker API to run in a separate background thread inside the editor tab without blocking your interaction with the code.
|
|
||||||
* CSSLint is heavily modified compared to the effectively frozen original one and supports various CSS3 features as well as CSS4 Color and CSS Grid syntax.
|
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
1. [Chrome Web Store](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne) (or [beta](https://chrome.google.com/webstore/detail/stylus-beta/apmmpaebfobifelkijhaljbmpcgbjbdo))
|
1. [Chrome Web Store](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne)
|
||||||
2. [Firefox add-ons](https://addons.mozilla.org/firefox/addon/styl-us/)
|
2. [Opera add-ons](https://addons.opera.com/extensions/details/stylus/)
|
||||||
3. [Opera add-ons](https://addons.opera.com/extensions/details/stylus/) (see [wiki](https://github.com/openstyles/stylus/wiki/Opera,-Outdated-Stylus) for more recent version)
|
3. [Firefox add-ons](https://addons.mozilla.org/firefox/addon/styl-us/)
|
||||||
|
|
||||||
## Screenshots
|
## Screen shot
|
||||||
|
|
||||||
Manager | Editor | Popup search | Popup config | Manager config | Options
|
![screen shot](https://cloud.githubusercontent.com/assets/11704051/24002324/aefd19fe-0a75-11e7-8160-d8731d2a6d03.png)
|
||||||
-|-|-|-|-|-
|
|
||||||
![Style manager](https://user-images.githubusercontent.com/1310400/34453460-214eaa5c-ed67-11e7-843b-d8960b71db6e.png) | ![Style editor](https://user-images.githubusercontent.com/1310400/34459585-3932cd94-ee05-11e7-9a1b-679522dddfb3.png) | ![Popup inline search](https://user-images.githubusercontent.com/1310400/34453463-21a44368-ed67-11e7-93b2-e1c8f5aac868.png) | ![Popup config for usercss](https://user-images.githubusercontent.com/1310400/34453462-218a589a-ed67-11e7-9040-7d0469eeadc3.png) | ![Style manager config for usercss](https://user-images.githubusercontent.com/1310400/34453464-21bdaf9c-ed67-11e7-8517-62d2f02e1918.png) | ![Options](https://user-images.githubusercontent.com/1310400/34453461-216aee4c-ed67-11e7-92db-ea21c1da5826.png)
|
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
|
|
||||||
* [Stylus help and FAQ in our Wiki](https://github.com/openstyles/stylus/wiki)
|
[![Discord][chat-image]][chat-link]
|
||||||
* [Discussion section](https://add0n.com/stylus.html#reviews) of our representation on add0n.com
|
|
||||||
* Discord: [![Discord][chat-image]][chat-link]
|
See the [help docs](http://userstyles.org/help/stylish_chrome) or [ask in userstyles.org forum](https://forum.userstyles.org). For Stylus specific questions and suggestions please use [review section](http://add0n.com/stylus.html#reviews) of the FAQs page.
|
||||||
|
|
||||||
[chat-image]: https://img.shields.io/discord/379521691774353408.svg
|
[chat-image]: https://img.shields.io/discord/379521691774353408.svg
|
||||||
[chat-link]: https://discordapp.com/widget?id=379521691774353408
|
[chat-link]: https://discordapp.com/widget?id=379521691774353408
|
||||||
|
@ -38,21 +23,17 @@ Manager | Editor | Popup search | Popup config | Manager config | Options
|
||||||
|
|
||||||
The source is hosted on [GitHub](https://github.com/openstyles/stylus) and pull requests are welcome.
|
The source is hosted on [GitHub](https://github.com/openstyles/stylus) and pull requests are welcome.
|
||||||
|
|
||||||
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus).
|
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/openstyles/stylus/issues).
|
||||||
|
|
||||||
See our [contributing](./.github/CONTRIBUTING.md) page for more details.
|
See our [contributing](./.github/CONTRIBUTING.md) page for more details.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Inherited code from the original [Stylish](https://github.com/stylish-userstyles/stylish/):
|
For copyright status of the "codemirror" directory, see [codemirror/LICENSE](https://github.com/openstyles/stylus/blob/master/src/vendor/codemirror/LICENSE). Everything else is:
|
||||||
|
|
||||||
Copyright © 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
|
Copyright (C) 2005-2014 Jason Barnabe <jason.barnabe@gmail.com>
|
||||||
|
|
||||||
Current Stylus:
|
Copyright (C) 2017 Stylus Team
|
||||||
|
|
||||||
Copyright © 2017-2022 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
|
|
||||||
|
|
||||||
**[GNU GPLv3](./LICENSE)**
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -65,8 +46,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
### External libraries
|
|
||||||
|
|
||||||
The licenses of [external libraries](./vendor) used in this project or [modified versions of external libraries](./vendor-overwrites) can be found in their respective directory.
|
|
||||||
|
|
|
@ -1,178 +1,210 @@
|
||||||
{
|
{
|
||||||
"InaccessibleFileHint": {
|
"appliesToEverything": {
|
||||||
"message": "Stylus لا يستطيع الوصول الى بعض انواع الملفات ( ملفات pdf و json )"
|
"message": "كل شيء",
|
||||||
},
|
"description": "Text displayed for styles that apply to all sites"
|
||||||
"addStyleLabel": {
|
},
|
||||||
"message": "كتابة نمط جديد"
|
"enableStyleLabel": {
|
||||||
},
|
"message": "تمكين",
|
||||||
"addStyleTitle": {
|
"description": "Label for the button to enable a style"
|
||||||
"message": "إضافة نمط"
|
},
|
||||||
},
|
"styleMissingName": {
|
||||||
"appliesAdd": {
|
"message": "أدخل اسمًا",
|
||||||
"message": "إضافة"
|
"description": "Error displayed when user saves without providing a name"
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDomainOption": {
|
||||||
"message": "ينطبق على: $applies$",
|
"message": "عناوين URL في النطاق",
|
||||||
"placeholders": {
|
"description": "Option to make the style apply to the entered string as a domain"
|
||||||
"applies": {
|
},
|
||||||
"content": "$1"
|
"checkForUpdate": {
|
||||||
}
|
"message": "البحث عن تحديث",
|
||||||
}
|
"description": "Label for the button to check a single style for an update"
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"helpAlt": {
|
||||||
"message": "و المزيد"
|
"message": "مساعدة",
|
||||||
},
|
"description": "Alternate text for help buttons"
|
||||||
"appliesDomainOption": {
|
},
|
||||||
"message": "عناوين URL في النطاق"
|
"findStylesForSite": {
|
||||||
},
|
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا",
|
||||||
"appliesHelp": {
|
"description": "Text for a link that gets a list of styles for the current site"
|
||||||
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم."
|
},
|
||||||
},
|
"manageHeading": {
|
||||||
"appliesLabel": {
|
"message": "الأنماط المثبتة",
|
||||||
"message": "ينطبق على"
|
"description": "Heading for the manage page"
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"styleEnabledLabel": {
|
||||||
"message": "عناوين URL التي تطابق regexp"
|
"message": "ممكّن",
|
||||||
},
|
"description": "Label for the enabled state of styles"
|
||||||
"appliesRemove": {
|
},
|
||||||
"message": "إزالة"
|
"styleToMozillaFormatHelp": {
|
||||||
},
|
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org.",
|
||||||
"appliesSpecify": {
|
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||||
"message": "تحديد"
|
},
|
||||||
},
|
"sectionAdd": {
|
||||||
"appliesToEverything": {
|
"message": "إضافة قسم آخر",
|
||||||
"message": "كل شيء"
|
"description": "Label for the button to add a section"
|
||||||
},
|
},
|
||||||
"appliesUrlOption": {
|
"styleSaveLabel": {
|
||||||
"message": "عنوان URL"
|
"message": "حفظ",
|
||||||
},
|
"description": "Label for save button for style editing"
|
||||||
"appliesUrlPrefixOption": {
|
},
|
||||||
"message": "عناوين URL البادئة بـ"
|
"appliesAdd": {
|
||||||
},
|
"message": "إضافة",
|
||||||
"checkAllUpdates": {
|
"description": "Label for the button to add an 'applies' entry"
|
||||||
"message": "البحث عن تحديثات لكل الأنماط"
|
},
|
||||||
},
|
"appliesRegexpOption": {
|
||||||
"checkForUpdate": {
|
"message": "عناوين URL التي تطابق regexp",
|
||||||
"message": "البحث عن تحديث"
|
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"styleInstall": {
|
||||||
"message": "جارٍ البحث..."
|
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
||||||
},
|
"description": "Confirmation when installing a style",
|
||||||
"confirmDelete": {
|
"placeholders": {
|
||||||
"message": "حذف"
|
"stylename": {
|
||||||
},
|
"content": "$1"
|
||||||
"confirmSave": {
|
}
|
||||||
"message": "حفظ"
|
}
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"disableStyleLabel": {
|
||||||
"message": "هل تريد بالتأكيد حذف هذا النمط؟"
|
"message": "تعطيل",
|
||||||
},
|
"description": "Label for the button to disable a style"
|
||||||
"deleteStyleLabel": {
|
},
|
||||||
"message": "حذف"
|
"styleCancelEditLabel": {
|
||||||
},
|
"message": "رجوع للإدارة",
|
||||||
"description": {
|
"description": "Label for cancel button for style editing"
|
||||||
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى."
|
},
|
||||||
},
|
"styleChangesNotSaved": {
|
||||||
"disableStyleLabel": {
|
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها.",
|
||||||
"message": "تعطيل"
|
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||||
},
|
},
|
||||||
"editDeleteText": {
|
"updateCheckFailServerUnreachable": {
|
||||||
"message": "حذف"
|
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه.",
|
||||||
},
|
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||||
"editStyleHeading": {
|
},
|
||||||
"message": "تعديل النمط"
|
"deleteStyleConfirm": {
|
||||||
},
|
"message": "هل تريد بالتأكيد حذف هذا النمط؟",
|
||||||
"editStyleLabel": {
|
"description": "Confirmation before deleting a style"
|
||||||
"message": "تعديل"
|
},
|
||||||
},
|
"appliesDisplay": {
|
||||||
"editStyleTitle": {
|
"message": "ينطبق على: $applies$",
|
||||||
"message": "تعديل النمط $stylename$",
|
"description": "Text on the manage screen to describe what the style applies to",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"styleSectionsTitle": {
|
||||||
"message": "تمكين"
|
"message": "الأقسام",
|
||||||
},
|
"description": "Title for the style sections section"
|
||||||
"genericAdd": {
|
},
|
||||||
"message": "إضافة"
|
"editStyleTitle": {
|
||||||
},
|
"message": "تعديل النمط $stylename$",
|
||||||
"genericEnabledLabel": {
|
"description": "Title of the page for editing styles",
|
||||||
"message": "ممكّن"
|
"placeholders": {
|
||||||
},
|
"stylename": {
|
||||||
"helpAlt": {
|
"content": "$1"
|
||||||
"message": "مساعدة"
|
}
|
||||||
},
|
}
|
||||||
"installUpdate": {
|
},
|
||||||
"message": "تثبيت التحديث"
|
"updateCheckSucceededNoUpdate": {
|
||||||
},
|
"message": "النمط محدّث.",
|
||||||
"manageHeading": {
|
"description": "Text that displays when an update check completed and no update is available"
|
||||||
"message": "الأنماط المثبتة"
|
},
|
||||||
},
|
"appliesUrlPrefixOption": {
|
||||||
"noStylesForSite": {
|
"message": "عناوين URL البادئة بـ",
|
||||||
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا."
|
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||||
},
|
},
|
||||||
"openManage": {
|
"sectionHelp": {
|
||||||
"message": "إدارة الأنماط المثبتة"
|
"message": "تتيح لك الأقسام تحديد أجزاء مختلفة من الرمز لتطبيقها على مجموعات مختلفة من عناوين URL بالنمط نفسه. فعلى سبيل المثال، يمكن لنمط مفرد تغيير الصفحة الرئيسية لموقع ويب بطريقة، مع تغيير بقية أجزاء موقع الويب بطريقة أخرى.",
|
||||||
},
|
"description": "Help text for sections"
|
||||||
"optionsSyncUrl": {
|
},
|
||||||
"message": "عنوان URL"
|
"noStylesForSite": {
|
||||||
},
|
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا.",
|
||||||
"sectionAdd": {
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
"message": "إضافة قسم آخر"
|
},
|
||||||
},
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"sectionCode": {
|
"message": "والمزيد",
|
||||||
"message": "الرمز"
|
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"appliesRemove": {
|
||||||
"message": "إزالة القسم"
|
"message": "إزالة",
|
||||||
},
|
"description": "Label for the button to remove an 'applies' entry"
|
||||||
"sections": {
|
},
|
||||||
"message": "الأقسام"
|
"appliesLabel": {
|
||||||
},
|
"message": "ينطبق على",
|
||||||
"styleCancelEditLabel": {
|
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||||
"message": "رجوع للإدارة"
|
},
|
||||||
},
|
"openManage": {
|
||||||
"styleChangesNotSaved": {
|
"message": "إدارة الأنماط المثبتة",
|
||||||
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها."
|
"description": "Link to open the manage page."
|
||||||
},
|
},
|
||||||
"styleEnabledLabel": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "ممكّن"
|
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
|
||||||
},
|
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||||
"styleInstall": {
|
"placeholders": {
|
||||||
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
"code": {
|
||||||
"placeholders": {
|
"content": "$1"
|
||||||
"stylename": {
|
}
|
||||||
"content": "$1"
|
}
|
||||||
}
|
},
|
||||||
}
|
"appliesSpecify": {
|
||||||
},
|
"message": "تحديد",
|
||||||
"styleMissingName": {
|
"description": "Label for the button to make a style apply only to specific sites"
|
||||||
"message": "أدخل اسمًا"
|
},
|
||||||
},
|
"installUpdate": {
|
||||||
"styleSaveLabel": {
|
"message": "تثبيت التحديث",
|
||||||
"message": "حفظ"
|
"description": "Label for the button to install an update for a single style"
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"sectionRemove": {
|
||||||
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org."
|
"message": "إزالة القسم",
|
||||||
},
|
"description": "Label for the button to remove a section"
|
||||||
"updateCheckFailBadResponseCode": {
|
},
|
||||||
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
|
"updateCompleted": {
|
||||||
"placeholders": {
|
"message": "اكتمل التحديث.",
|
||||||
"code": {
|
"description": "Text that displays when an update completed"
|
||||||
"content": "$1"
|
},
|
||||||
}
|
"checkingForUpdate": {
|
||||||
}
|
"message": "جارٍ البحث...",
|
||||||
},
|
"description": "Text to display when checking a style for an update"
|
||||||
"updateCheckFailServerUnreachable": {
|
},
|
||||||
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه."
|
"sectionCode": {
|
||||||
},
|
"message": "الرمز",
|
||||||
"updateCheckSucceededNoUpdate": {
|
"description": "Label for the code for a section"
|
||||||
"message": "النمط محدّث."
|
},
|
||||||
},
|
"appliesHelp": {
|
||||||
"updateCompleted": {
|
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم.",
|
||||||
"message": "اكتمل التحديث."
|
"description": "Help text for 'applies to' section"
|
||||||
}
|
},
|
||||||
}
|
"editStyleHeading": {
|
||||||
|
"message": "تعديل النمط",
|
||||||
|
"description": "Title of the page for editing styles"
|
||||||
|
},
|
||||||
|
"appliesUrlOption": {
|
||||||
|
"message": "عنوان URL",
|
||||||
|
"description": "Option to make the style apply to the entered string as a URL"
|
||||||
|
},
|
||||||
|
"addStyleTitle": {
|
||||||
|
"message": "إضافة نمط",
|
||||||
|
"description": "Title of the page for adding styles"
|
||||||
|
},
|
||||||
|
"checkAllUpdates": {
|
||||||
|
"message": "البحث عن تحديثات لكل الأنماط",
|
||||||
|
"description": "Label for the button to check all styles for updates"
|
||||||
|
},
|
||||||
|
"deleteStyleLabel": {
|
||||||
|
"message": "حذف",
|
||||||
|
"description": "Label for the button to delete a style"
|
||||||
|
},
|
||||||
|
"addStyleLabel": {
|
||||||
|
"message": "كتابة نمط جديد",
|
||||||
|
"description": "Label for the button to go to the add style page"
|
||||||
|
},
|
||||||
|
"editStyleLabel": {
|
||||||
|
"message": "تعديل",
|
||||||
|
"description": "Label for the button to go to the edit style page"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى.",
|
||||||
|
"description": "Extension description"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,278 +0,0 @@
|
||||||
{
|
|
||||||
"addStyleLabel": {
|
|
||||||
"message": "Напиши нов стил"
|
|
||||||
},
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Добави стил"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Добави"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Прилага се към: $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "и още"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "URLи на домейна"
|
|
||||||
},
|
|
||||||
"appliesHelp": {
|
|
||||||
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция."
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Прилага се към"
|
|
||||||
},
|
|
||||||
"appliesRegexpOption": {
|
|
||||||
"message": "Адреси, съвпадащи с regexp"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Премахни"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Уточни"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Всички"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URL започващи с"
|
|
||||||
},
|
|
||||||
"applyAllUpdates": {
|
|
||||||
"message": "Приложи всички промени"
|
|
||||||
},
|
|
||||||
"checkAllUpdates": {
|
|
||||||
"message": "Провери всички стилове за обновления"
|
|
||||||
},
|
|
||||||
"checkForUpdate": {
|
|
||||||
"message": "Провери за обновление"
|
|
||||||
},
|
|
||||||
"checkingForUpdate": {
|
|
||||||
"message": "Проверявам..."
|
|
||||||
},
|
|
||||||
"cm_indentWithTabs": {
|
|
||||||
"message": "Използвай табулация с умно отместване"
|
|
||||||
},
|
|
||||||
"cm_keyMap": {
|
|
||||||
"message": "Клавишни комбинации"
|
|
||||||
},
|
|
||||||
"cm_lineWrapping": {
|
|
||||||
"message": "Автоматично пренасяне"
|
|
||||||
},
|
|
||||||
"cm_smartIndent": {
|
|
||||||
"message": "Използвай умно отместване"
|
|
||||||
},
|
|
||||||
"cm_tabSize": {
|
|
||||||
"message": "Размер на табулацията"
|
|
||||||
},
|
|
||||||
"cm_theme": {
|
|
||||||
"message": "Тема"
|
|
||||||
},
|
|
||||||
"confirmNo": {
|
|
||||||
"message": "Не"
|
|
||||||
},
|
|
||||||
"confirmStop": {
|
|
||||||
"message": "Спри"
|
|
||||||
},
|
|
||||||
"confirmYes": {
|
|
||||||
"message": "Да"
|
|
||||||
},
|
|
||||||
"dbError": {
|
|
||||||
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?"
|
|
||||||
},
|
|
||||||
"defaultTheme": {
|
|
||||||
"message": "по подразбиране"
|
|
||||||
},
|
|
||||||
"deleteStyleConfirm": {
|
|
||||||
"message": "Наистина ли искаш да изтриеш този стил?"
|
|
||||||
},
|
|
||||||
"deleteStyleLabel": {
|
|
||||||
"message": "Изтрий"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове."
|
|
||||||
},
|
|
||||||
"disableAllStyles": {
|
|
||||||
"message": "Изключи всички стилове"
|
|
||||||
},
|
|
||||||
"disableStyleLabel": {
|
|
||||||
"message": "Забрани"
|
|
||||||
},
|
|
||||||
"editGotoLine": {
|
|
||||||
"message": "Иди на ред (или ред:кол)"
|
|
||||||
},
|
|
||||||
"editStyleHeading": {
|
|
||||||
"message": "Промени стила"
|
|
||||||
},
|
|
||||||
"editStyleLabel": {
|
|
||||||
"message": "Редактирай"
|
|
||||||
},
|
|
||||||
"editStyleTitle": {
|
|
||||||
"message": "Редактирай стил $stylename$",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enableStyleLabel": {
|
|
||||||
"message": "Разреши"
|
|
||||||
},
|
|
||||||
"exportLabel": {
|
|
||||||
"message": "Експорт"
|
|
||||||
},
|
|
||||||
"helpAlt": {
|
|
||||||
"message": "Помощ"
|
|
||||||
},
|
|
||||||
"helpKeyMapCommand": {
|
|
||||||
"message": "Напиши име на команда"
|
|
||||||
},
|
|
||||||
"helpKeyMapHotkey": {
|
|
||||||
"message": "Натисни клавишна комбинация"
|
|
||||||
},
|
|
||||||
"importAppendLabel": {
|
|
||||||
"message": "Добави към стил"
|
|
||||||
},
|
|
||||||
"importAppendTooltip": {
|
|
||||||
"message": "Добави импортирания стил към текущия"
|
|
||||||
},
|
|
||||||
"importLabel": {
|
|
||||||
"message": "Импорт"
|
|
||||||
},
|
|
||||||
"importReplaceLabel": {
|
|
||||||
"message": "Презапиши стила"
|
|
||||||
},
|
|
||||||
"importReplaceTooltip": {
|
|
||||||
"message": "Презапишете съдържанието на текущия стил с импортирания"
|
|
||||||
},
|
|
||||||
"installButton": {
|
|
||||||
"message": "Инсталирай стил"
|
|
||||||
},
|
|
||||||
"installButtonInstalled": {
|
|
||||||
"message": "Стилът е инсталиран"
|
|
||||||
},
|
|
||||||
"installButtonReinstall": {
|
|
||||||
"message": "Преинсталирай стила"
|
|
||||||
},
|
|
||||||
"installButtonUpdate": {
|
|
||||||
"message": "Обнови стила"
|
|
||||||
},
|
|
||||||
"installUpdate": {
|
|
||||||
"message": "Инсталирай обновление"
|
|
||||||
},
|
|
||||||
"installUpdateFrom": {
|
|
||||||
"message": "В момента стилът се обновява от $url$",
|
|
||||||
"placeholders": {
|
|
||||||
"url": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"installUpdateFromLabel": {
|
|
||||||
"message": "Провери за обновления"
|
|
||||||
},
|
|
||||||
"license": {
|
|
||||||
"message": "Лиценз"
|
|
||||||
},
|
|
||||||
"linkGetHelp": {
|
|
||||||
"message": "Получете помощ"
|
|
||||||
},
|
|
||||||
"linkGetStyles": {
|
|
||||||
"message": "Вземете стилове"
|
|
||||||
},
|
|
||||||
"linkTranslate": {
|
|
||||||
"message": "Преведете"
|
|
||||||
},
|
|
||||||
"linterCSSLintIncompatible": {
|
|
||||||
"message": "CSSLint не поддържа $preprocessorname$ preprocessor",
|
|
||||||
"placeholders": {
|
|
||||||
"preprocessorname": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterCSSLintSettings": {
|
|
||||||
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)"
|
|
||||||
},
|
|
||||||
"linterConfigPopupTitle": {
|
|
||||||
"message": "Настройте конфигурация за $linter$ правила",
|
|
||||||
"placeholders": {
|
|
||||||
"linter": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterConfigTooltip": {
|
|
||||||
"message": "Щракнете, за да конфигурирате този linter"
|
|
||||||
},
|
|
||||||
"linterInvalidConfigError": {
|
|
||||||
"message": "Не е записано заради тези неправилни настройки"
|
|
||||||
},
|
|
||||||
"linterIssues": {
|
|
||||||
"message": "Проблеми"
|
|
||||||
},
|
|
||||||
"linterIssuesHelp": {
|
|
||||||
"message": "Тези проблеми бяха намерени от $link$:",
|
|
||||||
"placeholders": {
|
|
||||||
"link": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterJSONError": {
|
|
||||||
"message": "Невалиден JSON формат"
|
|
||||||
},
|
|
||||||
"linterResetMessage": {
|
|
||||||
"message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец"
|
|
||||||
},
|
|
||||||
"linterRulesLink": {
|
|
||||||
"message": "Вижте пълния списък с правила"
|
|
||||||
},
|
|
||||||
"liveReloadError": {
|
|
||||||
"message": "Получи се грешка докато наблюдавахме файла"
|
|
||||||
},
|
|
||||||
"liveReloadLabel": {
|
|
||||||
"message": "Преглед на живо"
|
|
||||||
},
|
|
||||||
"manageFilters": {
|
|
||||||
"message": "Филтри"
|
|
||||||
},
|
|
||||||
"manageHeading": {
|
|
||||||
"message": "Инсталирани стилове"
|
|
||||||
},
|
|
||||||
"manageNewStyleAsUsercss": {
|
|
||||||
"message": "като Потребителскиcss"
|
|
||||||
},
|
|
||||||
"manageNewUI": {
|
|
||||||
"message": "Нова подредба на UI"
|
|
||||||
},
|
|
||||||
"manageOnlyDisabled": {
|
|
||||||
"message": "Само забранените стилове"
|
|
||||||
},
|
|
||||||
"manageOnlyEnabled": {
|
|
||||||
"message": "Само разрешените стилове"
|
|
||||||
},
|
|
||||||
"manageOnlyExternal": {
|
|
||||||
"message": "Само външните стилове"
|
|
||||||
},
|
|
||||||
"manageOnlyLocal": {
|
|
||||||
"message": "Само локалните стилове"
|
|
||||||
},
|
|
||||||
"manageOnlyLocalTooltip": {
|
|
||||||
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)"
|
|
||||||
},
|
|
||||||
"manageOnlyNonUsercss": {
|
|
||||||
"message": "Само не-Потребителскитеcss стилове"
|
|
||||||
},
|
|
||||||
"manageOnlyUpdates": {
|
|
||||||
"message": "Само с обновления или проблеми"
|
|
||||||
},
|
|
||||||
"manageOnlyUsercss": {
|
|
||||||
"message": "Само Потребителскиcss стилове"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,100 +0,0 @@
|
||||||
{
|
|
||||||
"addStyleLabel": {
|
|
||||||
"message": "Skriv ny stil"
|
|
||||||
},
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Tilføj stil"
|
|
||||||
},
|
|
||||||
"alphaChannel": {
|
|
||||||
"message": "Gennemsigtighed"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Tilføj"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Anvendes på: $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "og mere"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "URL'er på domænet"
|
|
||||||
},
|
|
||||||
"appliesHelp": {
|
|
||||||
"message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på."
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Anvendes på"
|
|
||||||
},
|
|
||||||
"appliesLineWidgetLabel": {
|
|
||||||
"message": "Vis 'Anvendes på'-info"
|
|
||||||
},
|
|
||||||
"appliesRegexpOption": {
|
|
||||||
"message": "URL'er der matcher regexp'en"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Fjern"
|
|
||||||
},
|
|
||||||
"appliesRemoveError": {
|
|
||||||
"message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Specificér"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Alt"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URL'er der starter med"
|
|
||||||
},
|
|
||||||
"applyAllUpdates": {
|
|
||||||
"message": "Anvend alle opdateringer"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"message": "Forfatter"
|
|
||||||
},
|
|
||||||
"bckpInstStyles": {
|
|
||||||
"message": "Eksportér stil"
|
|
||||||
},
|
|
||||||
"checkAllUpdates": {
|
|
||||||
"message": "Tjek alle stiler for opdateringer"
|
|
||||||
},
|
|
||||||
"checkAllUpdatesForce": {
|
|
||||||
"message": "Tjek igen, jeg redigerede ikke nogen stil!"
|
|
||||||
},
|
|
||||||
"checkForUpdate": {
|
|
||||||
"message": "Tjek efter opdatering"
|
|
||||||
},
|
|
||||||
"checkingForUpdate": {
|
|
||||||
"message": "Tjekker..."
|
|
||||||
},
|
|
||||||
"clickToUninstall": {
|
|
||||||
"message": "Klik for at afinstallere"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBrackets": {
|
|
||||||
"message": "Luk automatisk paranteser og citationstegn"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBracketsTooltip": {
|
|
||||||
"message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\""
|
|
||||||
},
|
|
||||||
"cm_autocompleteOnTyping": {
|
|
||||||
"message": "Autoudfyld på indtastning"
|
|
||||||
},
|
|
||||||
"cm_colorpicker": {
|
|
||||||
"message": "Farvevælgere for CSS-farver"
|
|
||||||
},
|
|
||||||
"cm_indentWithTabs": {
|
|
||||||
"message": "Brug tabs med smart indrykning"
|
|
||||||
},
|
|
||||||
"cm_keyMap": {
|
|
||||||
"message": "Tastegenveje"
|
|
||||||
},
|
|
||||||
"genericAdd": {
|
|
||||||
"message": "Tilføj"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,64 +0,0 @@
|
||||||
{
|
|
||||||
"appliesRemoveError": {
|
|
||||||
"message": "Cannot remove last 'applies to' entry"
|
|
||||||
},
|
|
||||||
"checkAllUpdatesForce": {
|
|
||||||
"message": "Check again—I didn't edit any styles!"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBrackets": {
|
|
||||||
"message": "Auto-close brackets and quotes"
|
|
||||||
},
|
|
||||||
"cm_colorpicker": {
|
|
||||||
"message": "Colour pickers for CSS colours"
|
|
||||||
},
|
|
||||||
"cm_resizeGripHint": {
|
|
||||||
"message": "Double-click to maximise/restore the height"
|
|
||||||
},
|
|
||||||
"colorpickerTooltip": {
|
|
||||||
"message": "Open colour picker"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites."
|
|
||||||
},
|
|
||||||
"editGotoLine": {
|
|
||||||
"message": "Go to line (or line:col)"
|
|
||||||
},
|
|
||||||
"editStyleHeading": {
|
|
||||||
"message": "Edit style"
|
|
||||||
},
|
|
||||||
"license": {
|
|
||||||
"message": "Licence"
|
|
||||||
},
|
|
||||||
"manageFaviconsGray": {
|
|
||||||
"message": "Greyed out"
|
|
||||||
},
|
|
||||||
"optionsBadgeDisabled": {
|
|
||||||
"message": "Background colour when disabled"
|
|
||||||
},
|
|
||||||
"optionsBadgeNormal": {
|
|
||||||
"message": "Background colour"
|
|
||||||
},
|
|
||||||
"optionsUpdateImportNote": {
|
|
||||||
"message": "When importing style backups from an old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
|
|
||||||
},
|
|
||||||
"optionsUpdateInterval": {
|
|
||||||
"message": "Userstyle auto-update interval in hours (specify 0 to disable)"
|
|
||||||
},
|
|
||||||
"styleInstallFailed": {
|
|
||||||
"message": "Failed to install userstyle\n$error$",
|
|
||||||
"placeholders": {
|
|
||||||
"error": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleRegexpPartialExplanation": {
|
|
||||||
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> 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)."
|
|
||||||
},
|
|
||||||
"styleUpdateDiscardChanges": {
|
|
||||||
"message": "The style has been changed outside the editor. Would you like to reload the style?"
|
|
||||||
},
|
|
||||||
"usercssConfigIncomplete": {
|
|
||||||
"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:"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,184 +1,226 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"appliesToEverything": {
|
||||||
"message": "Uusi Tyyli"
|
"message": "Kaikki",
|
||||||
},
|
"description": "Text displayed for styles that apply to all sites"
|
||||||
"addStyleTitle": {
|
},
|
||||||
"message": "Lisää Tyyli"
|
"enableStyleLabel": {
|
||||||
},
|
"message": "Aktivoi",
|
||||||
"appliesAdd": {
|
"description": "Label for the button to enable a style"
|
||||||
"message": "Lisää"
|
},
|
||||||
},
|
"styleMissingName": {
|
||||||
"appliesDisplay": {
|
"message": "Syötä nimi",
|
||||||
"message": "Kooskee: $applies$",
|
"description": "Error displayed when user saves without providing a name"
|
||||||
"placeholders": {
|
},
|
||||||
"applies": {
|
"appliesDomainOption": {
|
||||||
"content": "$1"
|
"message": "URL ositteita domainilla",
|
||||||
}
|
"description": "Option to make the style apply to the entered string as a domain"
|
||||||
}
|
},
|
||||||
},
|
"checkForUpdate": {
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"message": "Hae päivityksiä",
|
||||||
"message": "ja lisää"
|
"description": "Label for the button to check a single style for an update"
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"updateAllCheckSucceededNoUpdate": {
|
||||||
"message": "URL ositteita domainilla"
|
"message": "All styles are up to date.",
|
||||||
},
|
"description": "Text that displays when an update all check completed and no updates are available"
|
||||||
"appliesHelp": {
|
},
|
||||||
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee."
|
"helpAlt": {
|
||||||
},
|
"message": "Apu",
|
||||||
"appliesLabel": {
|
"description": "Alternate text for help buttons"
|
||||||
"message": "Koskee"
|
},
|
||||||
},
|
"findStylesForSite": {
|
||||||
"appliesRegexpOption": {
|
"message": "Hae lisää tyylejä tälle sivustolle",
|
||||||
"message": "URL ositteet jotka vastaavat regexpiä"
|
"description": "Text for a link that gets a list of styles for the current site"
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"manageHeading": {
|
||||||
"message": "Poista"
|
"message": "Asennetut Tyylit",
|
||||||
},
|
"description": "Heading for the manage page"
|
||||||
"appliesSpecify": {
|
},
|
||||||
"message": "Tarkenna"
|
"styleEnabledLabel": {
|
||||||
},
|
"message": "Aktivoitu",
|
||||||
"appliesToEverything": {
|
"description": "Label for the enabled state of styles"
|
||||||
"message": "Kaikki"
|
},
|
||||||
},
|
"styleToMozillaFormatHelp": {
|
||||||
"appliesUrlPrefixOption": {
|
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin.",
|
||||||
"message": "URL osoitteet jotka alkavat"
|
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"sectionAdd": {
|
||||||
"message": "Tarkista kaikki tyylit päivityksien varalta"
|
"message": "Lisää uusi osio",
|
||||||
},
|
"description": "Label for the button to add a section"
|
||||||
"checkForUpdate": {
|
},
|
||||||
"message": "Hae päivityksiä"
|
"styleSaveLabel": {
|
||||||
},
|
"message": "Tallenna",
|
||||||
"checkingForUpdate": {
|
"description": "Label for save button for style editing"
|
||||||
"message": "Tarkistetaan..."
|
},
|
||||||
},
|
"appliesAdd": {
|
||||||
"confirmDelete": {
|
"message": "Lisää",
|
||||||
"message": "Poista"
|
"description": "Label for the button to add an 'applies' entry"
|
||||||
},
|
},
|
||||||
"confirmSave": {
|
"appliesRegexpOption": {
|
||||||
"message": "Tallenna"
|
"message": "URL ositteet jotka vastaavat regexpiä",
|
||||||
},
|
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||||
"deleteStyleConfirm": {
|
},
|
||||||
"message": "Oletko varma että haluat poistaa tämän tyylin?"
|
"styleInstall": {
|
||||||
},
|
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
||||||
"deleteStyleLabel": {
|
"description": "Confirmation when installing a style",
|
||||||
"message": "Poista"
|
"placeholders": {
|
||||||
},
|
"stylename": {
|
||||||
"description": {
|
"content": "$1"
|
||||||
"message": "Uudelleen stailaa netti Stylusillä, käyttäjän tyyli hallintapaneelilla. Stylus antaa sinun helposti asentaa teemoja ja skinejä palvelluille kuten Google, Facebook, YouTube, Orkut, ja monelle, monelle muulle sivustolle."
|
}
|
||||||
},
|
}
|
||||||
"disableStyleLabel": {
|
},
|
||||||
"message": "Poista Käytöstä"
|
"disableStyleLabel": {
|
||||||
},
|
"message": "Poista Käytöstä",
|
||||||
"editDeleteText": {
|
"description": "Label for the button to disable a style"
|
||||||
"message": "Poista"
|
},
|
||||||
},
|
"prefShowBadge": {
|
||||||
"editStyleHeading": {
|
"message": "Show number of styles active for the current site on the toolbar button",
|
||||||
"message": "Muokkaa Tyyliä"
|
"description": "Label for the checkbox controlling toolbar badge text."
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"styleCancelEditLabel": {
|
||||||
"message": "Muokkaa"
|
"message": "Takaisin hallintapaneeliin",
|
||||||
},
|
"description": "Label for cancel button for style editing"
|
||||||
"editStyleTitle": {
|
},
|
||||||
"message": "Muokkaa Tyyliä $stylename$",
|
"styleChangesNotSaved": {
|
||||||
"placeholders": {
|
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta.",
|
||||||
"stylename": {
|
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||||
"content": "$1"
|
},
|
||||||
}
|
"updateCheckFailServerUnreachable": {
|
||||||
}
|
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen.",
|
||||||
},
|
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||||
"enableStyleLabel": {
|
},
|
||||||
"message": "Aktivoi"
|
"deleteStyleConfirm": {
|
||||||
},
|
"message": "Oletko varma että haluat poistaa tämän tyylin?",
|
||||||
"genericAdd": {
|
"description": "Confirmation before deleting a style"
|
||||||
"message": "Lisää"
|
},
|
||||||
},
|
"styleBadRegexp": {
|
||||||
"genericEnabledLabel": {
|
"message": "Regexp ei kelpaa.",
|
||||||
"message": "Aktivoitu"
|
"description": "Validation message for a bad regexp in a style"
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"appliesDisplay": {
|
||||||
"message": "Apu"
|
"message": "Kooskee: $applies$",
|
||||||
},
|
"description": "Text on the manage screen to describe what the style applies to",
|
||||||
"installUpdate": {
|
"placeholders": {
|
||||||
"message": "Asenna päivitys"
|
"applies": {
|
||||||
},
|
"content": "$1"
|
||||||
"manageHeading": {
|
}
|
||||||
"message": "Asennetut Tyylit"
|
}
|
||||||
},
|
},
|
||||||
"manageTitle": {
|
"styleSectionsTitle": {
|
||||||
"message": "Tyylikäs"
|
"message": "Osiot",
|
||||||
},
|
"description": "Title for the style sections section"
|
||||||
"noStylesForSite": {
|
},
|
||||||
"message": "Ei asennettuja tyylejä tällä sivustolla."
|
"editStyleTitle": {
|
||||||
},
|
"message": "Muokkaa Tyyliä $stylename$",
|
||||||
"openManage": {
|
"description": "Title of the page for editing styles",
|
||||||
"message": "Hallitse asennettuja tyylejä"
|
"placeholders": {
|
||||||
},
|
"stylename": {
|
||||||
"popupStylesFirst": {
|
"content": "$1"
|
||||||
"message": "List styles before commands in the toolbar button menu"
|
}
|
||||||
},
|
}
|
||||||
"prefShowBadge": {
|
},
|
||||||
"message": "Show number of styles active for the current site on the toolbar button"
|
"updateCheckSucceededNoUpdate": {
|
||||||
},
|
"message": "Tyyli on ajan tasalla.",
|
||||||
"sectionAdd": {
|
"description": "Text that displays when an update check completed and no update is available"
|
||||||
"message": "Lisää uusi osio"
|
},
|
||||||
},
|
"appliesUrlPrefixOption": {
|
||||||
"sectionCode": {
|
"message": "URL osoitteet jotka alkavat",
|
||||||
"message": "Koodi"
|
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"popupStylesFirst": {
|
||||||
"message": "Poista osio"
|
"message": "List styles before commands in the toolbar button menu",
|
||||||
},
|
"description": "Label for the checkbox controlling section order in the popup."
|
||||||
"sections": {
|
},
|
||||||
"message": "Osiot"
|
"sectionHelp": {
|
||||||
},
|
"message": "Osiot antavat sinun tarkentaa koodin eri osia niin että ne koskevat eri URL osoitteita samassa tyylissä. Esimerkiksi, yksi tyyli voi muokata kotisivua yhdellä tavalla kun se muokkaa koko muuta sivustoa toisella tavalla.",
|
||||||
"styleBadRegexp": {
|
"description": "Help text for sections"
|
||||||
"message": "Regexp ei kelpaa."
|
},
|
||||||
},
|
"noStylesForSite": {
|
||||||
"styleCancelEditLabel": {
|
"message": "Ei asennettuja tyylejä tällä sivustolla.",
|
||||||
"message": "Takaisin hallintapaneeliin"
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
},
|
},
|
||||||
"styleChangesNotSaved": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta."
|
"message": "ja lisää",
|
||||||
},
|
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||||
"styleEnabledLabel": {
|
},
|
||||||
"message": "Aktivoitu"
|
"appliesRemove": {
|
||||||
},
|
"message": "Poista",
|
||||||
"styleInstall": {
|
"description": "Label for the button to remove an 'applies' entry"
|
||||||
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
},
|
||||||
"placeholders": {
|
"manageTitle": {
|
||||||
"stylename": {
|
"message": "Tyylikäs",
|
||||||
"content": "$1"
|
"description": "Title for the manage page"
|
||||||
}
|
},
|
||||||
}
|
"appliesLabel": {
|
||||||
},
|
"message": "Koskee",
|
||||||
"styleMissingName": {
|
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||||
"message": "Syötä nimi"
|
},
|
||||||
},
|
"openManage": {
|
||||||
"styleSaveLabel": {
|
"message": "Hallitse asennettuja tyylejä",
|
||||||
"message": "Tallenna"
|
"description": "Link to open the manage page."
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin."
|
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
|
||||||
},
|
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
"placeholders": {
|
||||||
"message": "All styles are up to date."
|
"code": {
|
||||||
},
|
"content": "$1"
|
||||||
"updateCheckFailBadResponseCode": {
|
}
|
||||||
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
|
}
|
||||||
"placeholders": {
|
},
|
||||||
"code": {
|
"appliesSpecify": {
|
||||||
"content": "$1"
|
"message": "Tarkenna",
|
||||||
}
|
"description": "Label for the button to make a style apply only to specific sites"
|
||||||
}
|
},
|
||||||
},
|
"installUpdate": {
|
||||||
"updateCheckFailServerUnreachable": {
|
"message": "Asenna päivitys",
|
||||||
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen."
|
"description": "Label for the button to install an update for a single style"
|
||||||
},
|
},
|
||||||
"updateCheckSucceededNoUpdate": {
|
"sectionRemove": {
|
||||||
"message": "Tyyli on ajan tasalla."
|
"message": "Poista osio",
|
||||||
},
|
"description": "Label for the button to remove a section"
|
||||||
"updateCompleted": {
|
},
|
||||||
"message": "Päivitys suoritettu."
|
"updateCompleted": {
|
||||||
}
|
"message": "Päivitys suoritettu.",
|
||||||
}
|
"description": "Text that displays when an update completed"
|
||||||
|
},
|
||||||
|
"checkingForUpdate": {
|
||||||
|
"message": "Tarkistetaan...",
|
||||||
|
"description": "Text to display when checking a style for an update"
|
||||||
|
},
|
||||||
|
"sectionCode": {
|
||||||
|
"message": "Koodi",
|
||||||
|
"description": "Label for the code for a section"
|
||||||
|
},
|
||||||
|
"appliesHelp": {
|
||||||
|
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee.",
|
||||||
|
"description": "Help text for 'applies to' section"
|
||||||
|
},
|
||||||
|
"editStyleHeading": {
|
||||||
|
"message": "Muokkaa Tyyliä",
|
||||||
|
"description": "Title of the page for editing styles"
|
||||||
|
},
|
||||||
|
"addStyleTitle": {
|
||||||
|
"message": "Lisää Tyyli",
|
||||||
|
"description": "Title of the page for adding styles"
|
||||||
|
},
|
||||||
|
"checkAllUpdates": {
|
||||||
|
"message": "Tarkista kaikki tyylit päivityksien varalta",
|
||||||
|
"description": "Label for the button to check all styles for updates"
|
||||||
|
},
|
||||||
|
"deleteStyleLabel": {
|
||||||
|
"message": "Poista",
|
||||||
|
"description": "Label for the button to delete a style"
|
||||||
|
},
|
||||||
|
"addStyleLabel": {
|
||||||
|
"message": "Uusi Tyyli",
|
||||||
|
"description": "Label for the button to go to the add style page"
|
||||||
|
},
|
||||||
|
"editStyleLabel": {
|
||||||
|
"message": "Muokkaa",
|
||||||
|
"description": "Label for the button to go to the edit style page"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"message": "Uudelleen stailaa netti Stylusillä, käyttäjän tyyli hallintapaneelilla. Stylus antaa sinun helposti asentaa teemoja ja skinejä palvelluille kuten Google, Facebook, YouTube, Orkut, ja monelle, monelle muulle sivustolle.",
|
||||||
|
"description": "Extension description"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,97 +0,0 @@
|
||||||
{
|
|
||||||
"addStyleLabel": {
|
|
||||||
"message": "Nije styl skriuwe"
|
|
||||||
},
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Styl tafoegje"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Tafoegje"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Fan tapassing op: $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "en mear"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "URL’s op it domein"
|
|
||||||
},
|
|
||||||
"appliesHelp": {
|
|
||||||
"message": "Brûk de ‘Fan tapassing op’-funksjes om de URL’s foar de koade yn dizze seksje te beheinen."
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Fan tapassing op"
|
|
||||||
},
|
|
||||||
"appliesRegexpOption": {
|
|
||||||
"message": "URL’s oerienkommend mei de regexp"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Fuortsmite"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Spesifisearje"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Alles"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URL’s begjinnend mei"
|
|
||||||
},
|
|
||||||
"applyAllUpdates": {
|
|
||||||
"message": "Alle fernijingen tapasse"
|
|
||||||
},
|
|
||||||
"checkAllUpdates": {
|
|
||||||
"message": "Alle stilen kontrolearje op fernijingen"
|
|
||||||
},
|
|
||||||
"checkForUpdate": {
|
|
||||||
"message": "Kontrolearje op fernijing"
|
|
||||||
},
|
|
||||||
"checkingForUpdate": {
|
|
||||||
"message": "Kontrolearje..."
|
|
||||||
},
|
|
||||||
"cm_indentWithTabs": {
|
|
||||||
"message": "Ljepblêden mei tûke ynspringing brûke"
|
|
||||||
},
|
|
||||||
"cm_keyMap": {
|
|
||||||
"message": "Toetseboerdyndieling"
|
|
||||||
},
|
|
||||||
"cm_lineWrapping": {
|
|
||||||
"message": "Teksttebekrin"
|
|
||||||
},
|
|
||||||
"cm_smartIndent": {
|
|
||||||
"message": "Tûke ynspringing brûke"
|
|
||||||
},
|
|
||||||
"cm_tabSize": {
|
|
||||||
"message": "Ljepblêdgrutte"
|
|
||||||
},
|
|
||||||
"cm_theme": {
|
|
||||||
"message": "Tema"
|
|
||||||
},
|
|
||||||
"confirmNo": {
|
|
||||||
"message": "Nee"
|
|
||||||
},
|
|
||||||
"confirmStop": {
|
|
||||||
"message": "Stoppe"
|
|
||||||
},
|
|
||||||
"confirmYes": {
|
|
||||||
"message": "Ja"
|
|
||||||
},
|
|
||||||
"dbError": {
|
|
||||||
"message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?"
|
|
||||||
},
|
|
||||||
"defaultTheme": {
|
|
||||||
"message": "standert"
|
|
||||||
},
|
|
||||||
"deleteStyleConfirm": {
|
|
||||||
"message": "Binne jo wis dat jo dizze styl fuortsmite wolle?"
|
|
||||||
},
|
|
||||||
"deleteStyleLabel": {
|
|
||||||
"message": "Fuortsmite"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Engadir Estilo"
|
|
||||||
},
|
|
||||||
"alphaChannel": {
|
|
||||||
"message": "Opacidade"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Engadir"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Aplica a: $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "e mais"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "URLs no dominio"
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Aplica para"
|
|
||||||
},
|
|
||||||
"appliesLineWidgetWarning": {
|
|
||||||
"message": "Non funciona con CSS minificado"
|
|
||||||
},
|
|
||||||
"appliesRegexpOption": {
|
|
||||||
"message": "URLs que concorden co regexp"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Suprimir"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Especificar"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Todo"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URLs que comecen por"
|
|
||||||
},
|
|
||||||
"applyAllUpdates": {
|
|
||||||
"message": "Aplicar tódalas actualizacións"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"message": "Autor"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,860 +0,0 @@
|
||||||
{
|
|
||||||
"addStyleLabel": {
|
|
||||||
"message": "Scrieți o temă nouă"
|
|
||||||
},
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Adăugați o temă"
|
|
||||||
},
|
|
||||||
"alphaChannel": {
|
|
||||||
"message": "Opacitate"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Adăugați"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Aplicabil pentru: $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "mai mult"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "URLs din domain"
|
|
||||||
},
|
|
||||||
"appliesHelp": {
|
|
||||||
"message": "Folosiți 'Aplicabil pentru' pentru a limita pe ce URLs va fi aplicată temă."
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Aplicabil pentru"
|
|
||||||
},
|
|
||||||
"appliesLineWidgetLabel": {
|
|
||||||
"message": "Arată informații despre 'Aplicabil pentru'"
|
|
||||||
},
|
|
||||||
"appliesLineWidgetWarning": {
|
|
||||||
"message": "Nu funcționează cu CSS minimizat"
|
|
||||||
},
|
|
||||||
"appliesRegexpOption": {
|
|
||||||
"message": "URLs găsite cu regexp"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Ștergeți"
|
|
||||||
},
|
|
||||||
"appliesRemoveError": {
|
|
||||||
"message": "Nu se poate șterge 'aplicabil pentru'"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Specificați"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Totul"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URL-uri începănd cu"
|
|
||||||
},
|
|
||||||
"applyAllUpdates": {
|
|
||||||
"message": "Aplicați toate update-urile"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"message": "Autor"
|
|
||||||
},
|
|
||||||
"bckpInstStyles": {
|
|
||||||
"message": "Exportați teme"
|
|
||||||
},
|
|
||||||
"checkAllUpdates": {
|
|
||||||
"message": "Verificați toate temele pentru update-uri"
|
|
||||||
},
|
|
||||||
"checkAllUpdatesForce": {
|
|
||||||
"message": "Verifică iar, nu am modificat temele!"
|
|
||||||
},
|
|
||||||
"checkForUpdate": {
|
|
||||||
"message": "Verificați pentru update-uri"
|
|
||||||
},
|
|
||||||
"checkingForUpdate": {
|
|
||||||
"message": "Verficare..."
|
|
||||||
},
|
|
||||||
"clickToUninstall": {
|
|
||||||
"message": "Click pentru dezinstalare"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBrackets": {
|
|
||||||
"message": "Închideți automat parantezele și ghilimelele"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBracketsTooltip": {
|
|
||||||
"message": "Închideți automat când deschizideți una dintre ()[]{}''\"\""
|
|
||||||
},
|
|
||||||
"cm_autocompleteOnTyping": {
|
|
||||||
"message": "Autocompletare în timpul scrierii"
|
|
||||||
},
|
|
||||||
"cm_colorpicker": {
|
|
||||||
"message": "Alegere de culori pentru CSS"
|
|
||||||
},
|
|
||||||
"cm_indentWithTabs": {
|
|
||||||
"message": "Folosiți tab-uri cu autoindentare"
|
|
||||||
},
|
|
||||||
"cm_matchHighlightSelection": {
|
|
||||||
"message": "Doar selecție"
|
|
||||||
},
|
|
||||||
"cm_matchHighlightToken": {
|
|
||||||
"message": "Token-ul de sub cursor"
|
|
||||||
},
|
|
||||||
"cm_resizeGripHint": {
|
|
||||||
"message": "Dublu click pentru a maximiza sau restaura înălțimea"
|
|
||||||
},
|
|
||||||
"cm_selectByTokens": {
|
|
||||||
"message": "Dublu-click pentru a selecta simbolurile"
|
|
||||||
},
|
|
||||||
"cm_selectByTokensTooltip": {
|
|
||||||
"message": "Exemple de simboluri: .foo-bar-2 #aabbcc 0.32 !important\nCand inactiv: cuvinte delimitate cu punctuatie sunt selectate."
|
|
||||||
},
|
|
||||||
"cm_smartIndent": {
|
|
||||||
"message": "Folosiți indentare inteligentă"
|
|
||||||
},
|
|
||||||
"cm_tabSize": {
|
|
||||||
"message": "Mărimea tab-urilor"
|
|
||||||
},
|
|
||||||
"cm_theme": {
|
|
||||||
"message": "Temă pentru cod"
|
|
||||||
},
|
|
||||||
"colorpickerSwitchFormatTooltip": {
|
|
||||||
"message": "Schimbați formatul: HEX -> RGB -> HSL.\nShift-click pentru inversarea direcției.\nDe asemenea cu PgUp (PageUp), PgDn (PageDown)."
|
|
||||||
},
|
|
||||||
"colorpickerTooltip": {
|
|
||||||
"message": "Deschideți color picker"
|
|
||||||
},
|
|
||||||
"configOnChange": {
|
|
||||||
"message": "la schimbare"
|
|
||||||
},
|
|
||||||
"configOnChangeTooltip": {
|
|
||||||
"message": "Autosalvare și aplicare a modificărilor automată"
|
|
||||||
},
|
|
||||||
"configureStyle": {
|
|
||||||
"message": "Modificați"
|
|
||||||
},
|
|
||||||
"configureStyleOnHomepage": {
|
|
||||||
"message": "Modificați pe pagina homepage"
|
|
||||||
},
|
|
||||||
"confirmDefault": {
|
|
||||||
"message": "Folosiți default"
|
|
||||||
},
|
|
||||||
"confirmDelete": {
|
|
||||||
"message": "Ștergeți"
|
|
||||||
},
|
|
||||||
"confirmDiscardChanges": {
|
|
||||||
"message": "Pierdeți modificările?"
|
|
||||||
},
|
|
||||||
"confirmNo": {
|
|
||||||
"message": "Nu"
|
|
||||||
},
|
|
||||||
"confirmSave": {
|
|
||||||
"message": "Salvați"
|
|
||||||
},
|
|
||||||
"confirmYes": {
|
|
||||||
"message": "Da"
|
|
||||||
},
|
|
||||||
"dateInstalled": {
|
|
||||||
"message": "Data instalării"
|
|
||||||
},
|
|
||||||
"dateUpdated": {
|
|
||||||
"message": "Data updatării"
|
|
||||||
},
|
|
||||||
"dbError": {
|
|
||||||
"message": "O eroare în baza de date a apărut. Doriți să vizitați pagina web pentru o soluție?"
|
|
||||||
},
|
|
||||||
"deleteStyleConfirm": {
|
|
||||||
"message": "Sunteți sigur că doriți să ștergeți această temă?"
|
|
||||||
},
|
|
||||||
"deleteStyleLabel": {
|
|
||||||
"message": "Ștergeți"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"message": "Stilizați internetul cu Stylus, un manager de teme. Stylus vă permite să instalați cu ușurință teme pentru multe site-uri populare."
|
|
||||||
},
|
|
||||||
"disableAllStyles": {
|
|
||||||
"message": "Dezactivați toate temele"
|
|
||||||
},
|
|
||||||
"disableStyleLabel": {
|
|
||||||
"message": "Dezactivați"
|
|
||||||
},
|
|
||||||
"dragDropMessage": {
|
|
||||||
"message": "Drag and drop backup-ul oriunde pentru import."
|
|
||||||
},
|
|
||||||
"editDeleteText": {
|
|
||||||
"message": "Ștergeți"
|
|
||||||
},
|
|
||||||
"editGotoLine": {
|
|
||||||
"message": "Mergeți la linia"
|
|
||||||
},
|
|
||||||
"editStyleHeading": {
|
|
||||||
"message": "Modificați tema"
|
|
||||||
},
|
|
||||||
"editStyleLabel": {
|
|
||||||
"message": "Modificați"
|
|
||||||
},
|
|
||||||
"editStyleTitle": {
|
|
||||||
"message": "Modificați tema $stylename$",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enableStyleLabel": {
|
|
||||||
"message": "Activați"
|
|
||||||
},
|
|
||||||
"externalLink": {
|
|
||||||
"message": "Link extern"
|
|
||||||
},
|
|
||||||
"externalSupport": {
|
|
||||||
"message": "Suport"
|
|
||||||
},
|
|
||||||
"externalUsercssDocument": {
|
|
||||||
"message": "Documentație pentru Usercss"
|
|
||||||
},
|
|
||||||
"filteredStyles": {
|
|
||||||
"message": "$numShown$ vizualizabile din $numTotal$ ",
|
|
||||||
"placeholders": {
|
|
||||||
"numShown": {
|
|
||||||
"content": "$1"
|
|
||||||
},
|
|
||||||
"numTotal": {
|
|
||||||
"content": "$2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"filteredStylesAllHidden": {
|
|
||||||
"message": "Filtrele aplicate nu au găsit nicio temă"
|
|
||||||
},
|
|
||||||
"findStyles": {
|
|
||||||
"message": "Găsiți teme"
|
|
||||||
},
|
|
||||||
"genericAdd": {
|
|
||||||
"message": "Adaugă"
|
|
||||||
},
|
|
||||||
"genericClone": {
|
|
||||||
"message": "Clonează"
|
|
||||||
},
|
|
||||||
"genericDisabledLabel": {
|
|
||||||
"message": "Dezactivat"
|
|
||||||
},
|
|
||||||
"genericEnabledLabel": {
|
|
||||||
"message": "Activat"
|
|
||||||
},
|
|
||||||
"genericError": {
|
|
||||||
"message": "Eroare"
|
|
||||||
},
|
|
||||||
"genericHistoryLabel": {
|
|
||||||
"message": "Historic"
|
|
||||||
},
|
|
||||||
"genericNext": {
|
|
||||||
"message": "Următor"
|
|
||||||
},
|
|
||||||
"genericPrevious": {
|
|
||||||
"message": "Precedent"
|
|
||||||
},
|
|
||||||
"genericResetLabel": {
|
|
||||||
"message": "Resetați"
|
|
||||||
},
|
|
||||||
"genericSavedMessage": {
|
|
||||||
"message": "Salvat"
|
|
||||||
},
|
|
||||||
"genericTitle": {
|
|
||||||
"message": "Titlu"
|
|
||||||
},
|
|
||||||
"genericUnknown": {
|
|
||||||
"message": "Necunoscut"
|
|
||||||
},
|
|
||||||
"helpAlt": {
|
|
||||||
"message": "Ajutor"
|
|
||||||
},
|
|
||||||
"helpKeyMapCommand": {
|
|
||||||
"message": "Tastați un nume de comandă"
|
|
||||||
},
|
|
||||||
"helpKeyMapHotkey": {
|
|
||||||
"message": "Tastați o hotkey"
|
|
||||||
},
|
|
||||||
"importAppendLabel": {
|
|
||||||
"message": "Concatenare la temă"
|
|
||||||
},
|
|
||||||
"importAppendTooltip": {
|
|
||||||
"message": "Concatenați tema importată la cea curentă"
|
|
||||||
},
|
|
||||||
"importReplaceLabel": {
|
|
||||||
"message": "Scrieți peste tema curentă"
|
|
||||||
},
|
|
||||||
"importReplaceTooltip": {
|
|
||||||
"message": "Scrieți peste tema curentă conținutul temei importate"
|
|
||||||
},
|
|
||||||
"importReportLegendAdded": {
|
|
||||||
"message": "adăugat"
|
|
||||||
},
|
|
||||||
"importReportLegendIdentical": {
|
|
||||||
"message": "cele identice au fost sărite"
|
|
||||||
},
|
|
||||||
"importReportLegendInvalid": {
|
|
||||||
"message": "cele invalide au fost ignorate"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedBoth": {
|
|
||||||
"message": "meta info și codul au fost updatate"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedCode": {
|
|
||||||
"message": "cod updatat"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedMeta": {
|
|
||||||
"message": "meta info updatată"
|
|
||||||
},
|
|
||||||
"importReportTitle": {
|
|
||||||
"message": "Importul temelor finalizat"
|
|
||||||
},
|
|
||||||
"importReportUnchanged": {
|
|
||||||
"message": "Nimic nu a fost schimbat."
|
|
||||||
},
|
|
||||||
"importReportUndone": {
|
|
||||||
"message": "temele au fost înlocuite cu variantele precedente"
|
|
||||||
},
|
|
||||||
"importReportUndoneTitle": {
|
|
||||||
"message": "Importarea a fost anulată"
|
|
||||||
},
|
|
||||||
"installButton": {
|
|
||||||
"message": "Instalați tema"
|
|
||||||
},
|
|
||||||
"installButtonInstalled": {
|
|
||||||
"message": "Tema a fost instalată"
|
|
||||||
},
|
|
||||||
"installButtonReinstall": {
|
|
||||||
"message": "Reinstalați tema"
|
|
||||||
},
|
|
||||||
"installUpdate": {
|
|
||||||
"message": "Instalați update-uri"
|
|
||||||
},
|
|
||||||
"installUpdateFrom": {
|
|
||||||
"message": "Tema este updatată de la $url$",
|
|
||||||
"placeholders": {
|
|
||||||
"url": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"installUpdateFromLabel": {
|
|
||||||
"message": "Verificați update-urile"
|
|
||||||
},
|
|
||||||
"license": {
|
|
||||||
"message": "Licență"
|
|
||||||
},
|
|
||||||
"linkGetHelp": {
|
|
||||||
"message": "Găsiți ajutor"
|
|
||||||
},
|
|
||||||
"linkGetStyles": {
|
|
||||||
"message": "Căutați teme"
|
|
||||||
},
|
|
||||||
"linkTranslate": {
|
|
||||||
"message": "Traduce"
|
|
||||||
},
|
|
||||||
"linterCSSLintIncompatible": {
|
|
||||||
"message": "CSSLint nu suportă preprocesorul $preprocessorname$ ",
|
|
||||||
"placeholders": {
|
|
||||||
"preprocessorname": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterCSSLintSettings": {
|
|
||||||
"message": "(Setați regula ca: 0 = dezactivat; 1 = avertisment; 2 = eroare)"
|
|
||||||
},
|
|
||||||
"linterConfigPopupTitle": {
|
|
||||||
"message": "Setați $linter$ pentru configurarea regulilor",
|
|
||||||
"placeholders": {
|
|
||||||
"linter": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterConfigTooltip": {
|
|
||||||
"message": "Click pentru a configura linter-ul"
|
|
||||||
},
|
|
||||||
"linterInvalidConfigError": {
|
|
||||||
"message": "Nu a fost salvat din cauza acestor setări invalide:"
|
|
||||||
},
|
|
||||||
"linterIssues": {
|
|
||||||
"message": "Probleme"
|
|
||||||
},
|
|
||||||
"linterIssuesHelp": {
|
|
||||||
"message": "Aceste probleme au fost găsite te $link$:",
|
|
||||||
"placeholders": {
|
|
||||||
"link": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterResetMessage": {
|
|
||||||
"message": "Pentru a reveni la varianta precedentă, apăsați Ctrl-Z (sau Cmd-Z) în text box"
|
|
||||||
},
|
|
||||||
"linterRulesLink": {
|
|
||||||
"message": "Vizualizați lista completă de reguli"
|
|
||||||
},
|
|
||||||
"liveReloadError": {
|
|
||||||
"message": "A avut loc o eroare în timpul monitorizării acestui fișier"
|
|
||||||
},
|
|
||||||
"manageFavicons": {
|
|
||||||
"message": "Favicons în coloana 'se aplică la'"
|
|
||||||
},
|
|
||||||
"manageFaviconsGray": {
|
|
||||||
"message": "Hașurat"
|
|
||||||
},
|
|
||||||
"manageFaviconsHelp": {
|
|
||||||
"message": "Stylus folosește un serviciu extern https://icons.duckduckgo.com"
|
|
||||||
},
|
|
||||||
"manageFilters": {
|
|
||||||
"message": "Filtre"
|
|
||||||
},
|
|
||||||
"manageHeading": {
|
|
||||||
"message": "Teme instalate"
|
|
||||||
},
|
|
||||||
"manageMaxTargets": {
|
|
||||||
"message": "Numărul obiectelor la care se aplică"
|
|
||||||
},
|
|
||||||
"manageNewUI": {
|
|
||||||
"message": "UI layout pentru mesajele noi"
|
|
||||||
},
|
|
||||||
"manageOnlyDisabled": {
|
|
||||||
"message": "Doar teme dezactivate"
|
|
||||||
},
|
|
||||||
"manageOnlyEnabled": {
|
|
||||||
"message": "Doar temele activate"
|
|
||||||
},
|
|
||||||
"manageOnlyExternal": {
|
|
||||||
"message": "Doar teme externe"
|
|
||||||
},
|
|
||||||
"manageOnlyLocal": {
|
|
||||||
"message": "Doar temele create local"
|
|
||||||
},
|
|
||||||
"manageOnlyLocalTooltip": {
|
|
||||||
"message": "(temele neinstalate prin userstyles.org)"
|
|
||||||
},
|
|
||||||
"manageOnlyNonUsercss": {
|
|
||||||
"message": "Doar teme non-Usercss"
|
|
||||||
},
|
|
||||||
"manageOnlyUpdates": {
|
|
||||||
"message": "Doar cu update-uri sau erori"
|
|
||||||
},
|
|
||||||
"manageOnlyUsercss": {
|
|
||||||
"message": "Doar teme Usercss"
|
|
||||||
},
|
|
||||||
"menuShowBadge": {
|
|
||||||
"message": "Afișați numărul temelor active"
|
|
||||||
},
|
|
||||||
"meta_invalidCheckboxDefault": {
|
|
||||||
"message": "@var checkbox invalidă: valuarea trebuie să fie 0 sau 1"
|
|
||||||
},
|
|
||||||
"noStylesForSite": {
|
|
||||||
"message": "Nicio temă instalată pentru acest site."
|
|
||||||
},
|
|
||||||
"openManage": {
|
|
||||||
"message": "Managerul"
|
|
||||||
},
|
|
||||||
"openOptions": {
|
|
||||||
"message": "Opțiuni"
|
|
||||||
},
|
|
||||||
"openStylesManager": {
|
|
||||||
"message": "Deschideți managerul de teme"
|
|
||||||
},
|
|
||||||
"optionsActions": {
|
|
||||||
"message": "Acțiuni"
|
|
||||||
},
|
|
||||||
"optionsAdvanced": {
|
|
||||||
"message": "Avansat"
|
|
||||||
},
|
|
||||||
"optionsAdvancedContextDelete": {
|
|
||||||
"message": "Adaugați 'Ștergeți' în meniul de click dreapta"
|
|
||||||
},
|
|
||||||
"optionsAdvancedExposeIframes": {
|
|
||||||
"message": "Expuneți iframes via HTML[stylus-iframe]"
|
|
||||||
},
|
|
||||||
"optionsAdvancedNewStyleAsUsercss": {
|
|
||||||
"message": "Scrieți temă nouă în formatul usercss"
|
|
||||||
},
|
|
||||||
"optionsBadgeDisabled": {
|
|
||||||
"message": "Culoare de fundal pentru modul inactiv"
|
|
||||||
},
|
|
||||||
"optionsBadgeNormal": {
|
|
||||||
"message": "Culoare de fundal"
|
|
||||||
},
|
|
||||||
"optionsCheck": {
|
|
||||||
"message": "Updatați temele"
|
|
||||||
},
|
|
||||||
"optionsCheckUpdate": {
|
|
||||||
"message": "Verificați și instalează toate update-urile"
|
|
||||||
},
|
|
||||||
"optionsCustomizeBadge": {
|
|
||||||
"message": "Bulinuța de pe iconița din toolbar"
|
|
||||||
},
|
|
||||||
"optionsCustomizeUpdate": {
|
|
||||||
"message": "Update-uri"
|
|
||||||
},
|
|
||||||
"optionsHeading": {
|
|
||||||
"message": "Opțiuni"
|
|
||||||
},
|
|
||||||
"optionsIconDark": {
|
|
||||||
"message": "Teme întunecate pentru browser"
|
|
||||||
},
|
|
||||||
"optionsIconLight": {
|
|
||||||
"message": "Teme albe pentru browser"
|
|
||||||
},
|
|
||||||
"optionsOpen": {
|
|
||||||
"message": "Deschideți"
|
|
||||||
},
|
|
||||||
"optionsOpenManager": {
|
|
||||||
"message": "Managerul de teme"
|
|
||||||
},
|
|
||||||
"optionsPopupWidth": {
|
|
||||||
"message": "Mărimea ferestrei popup (în pixeli)"
|
|
||||||
},
|
|
||||||
"optionsReset": {
|
|
||||||
"message": "Resetați opțiunile"
|
|
||||||
},
|
|
||||||
"optionsResetButton": {
|
|
||||||
"message": "Resetați opțiunile"
|
|
||||||
},
|
|
||||||
"optionsSubheading": {
|
|
||||||
"message": "Mai multe opțiuni"
|
|
||||||
},
|
|
||||||
"optionsUpdateImportNote": {
|
|
||||||
"message": "Atunci când sunt importate teme din backup-uri din versiuni mai vechi sau din Stylish, verifică upate-urile manual pentru a fi sigur că toate temele sunt la zi."
|
|
||||||
},
|
|
||||||
"optionsUpdateInterval": {
|
|
||||||
"message": "Intervalul de autoupdate în ore (0 pentru dezactivat)"
|
|
||||||
},
|
|
||||||
"paginationCurrent": {
|
|
||||||
"message": "Pagina curentă"
|
|
||||||
},
|
|
||||||
"paginationEstimated": {
|
|
||||||
"message": "Număr estimat de pagini"
|
|
||||||
},
|
|
||||||
"paginationNext": {
|
|
||||||
"message": "Pagina următoare"
|
|
||||||
},
|
|
||||||
"paginationPrevious": {
|
|
||||||
"message": "Pagina precedentă"
|
|
||||||
},
|
|
||||||
"paginationTotal": {
|
|
||||||
"message": "Număr total de pagini"
|
|
||||||
},
|
|
||||||
"parseUsercssError": {
|
|
||||||
"message": "Stylus nu a putut analiza usercss-ul"
|
|
||||||
},
|
|
||||||
"popupBorders": {
|
|
||||||
"message": "Adăugați bordură albă pe margini"
|
|
||||||
},
|
|
||||||
"popupBordersTooltip": {
|
|
||||||
"message": "Folositor pentru teme întunecate in noul Chrome deoarece nu mai colorează bordurile"
|
|
||||||
},
|
|
||||||
"popupHotkeysInfo": {
|
|
||||||
"message": "<1>-<9>, <0>, de asemenea îm numpad - activează/dezactivează tema numărul N (0 este 10)\n<A>-<Z> activează/dezactivează prima temă cu un nume care începe cu o litera\n<Shift> deschide editorul și nu activează/dezactivează ceva\n<Numpad +> activează temele listate\n<Numpad –> dezactiveaza temele listate\n<Numpad *> și <`> (backtick) - activează/dezactivează temele activate inițial; nu se aplică altor teme activate când popup-ul este deschid pentru a permite restaurarea seleției inițiale pentru a permite testări; dezactivează tot, apoi activează/dezactivează, spre exemplu<Numpad –> <Numpad *>\nMai multe informații sunt avabile în wiki"
|
|
||||||
},
|
|
||||||
"popupHotkeysTooltip": {
|
|
||||||
"message": "Click pentru a vedea hotkeys avabile"
|
|
||||||
},
|
|
||||||
"popupManageTooltip": {
|
|
||||||
"message": "Shift-click sau right-click pentru a deschide managerurl cu teme aplicabile site-ului curent"
|
|
||||||
},
|
|
||||||
"popupOpenEditInWindow": {
|
|
||||||
"message": "Deschide editorul în alt window"
|
|
||||||
},
|
|
||||||
"popupOpenEditInWindowTooltip": {
|
|
||||||
"message": "Activ când tabul editorului este detașat de browser,\nși dezactivat când un singur tab cu editor este atașat la alt window."
|
|
||||||
},
|
|
||||||
"popupStylesFirst": {
|
|
||||||
"message": "Temele înaintea comenzilor"
|
|
||||||
},
|
|
||||||
"prefShowBadge": {
|
|
||||||
"message": "Numărul de teme active pentru site-ul curent"
|
|
||||||
},
|
|
||||||
"previewTooltip": {
|
|
||||||
"message": "Aplică temporar modificările fără a salva.\nSalvează tema pentru a face schimbările permanente."
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"message": "Înlocuiți"
|
|
||||||
},
|
|
||||||
"replaceAll": {
|
|
||||||
"message": "Înlocuiți tot"
|
|
||||||
},
|
|
||||||
"replaceWith": {
|
|
||||||
"message": "Înlocuiți cu"
|
|
||||||
},
|
|
||||||
"retrieveBckp": {
|
|
||||||
"message": "Importați teme"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"message": "Căutați"
|
|
||||||
},
|
|
||||||
"searchNumberOfResults": {
|
|
||||||
"message": "Număr de rezultate"
|
|
||||||
},
|
|
||||||
"searchNumberOfResults2": {
|
|
||||||
"message": "Număr de rezultate în cod și în 'aplicabil pentru'"
|
|
||||||
},
|
|
||||||
"searchRegexp": {
|
|
||||||
"message": "Folosiți sintaxă /re/ pentru căutare tip regexp"
|
|
||||||
},
|
|
||||||
"searchResultInstallCount": {
|
|
||||||
"message": "Număr total de instalări"
|
|
||||||
},
|
|
||||||
"searchResultNoneFound": {
|
|
||||||
"message": "Nicio tema găsită pentru acest site."
|
|
||||||
},
|
|
||||||
"searchResultUpdated": {
|
|
||||||
"message": "Updatate"
|
|
||||||
},
|
|
||||||
"searchResultWeeklyCount": {
|
|
||||||
"message": "Instalări săptămânale"
|
|
||||||
},
|
|
||||||
"sectionAdd": {
|
|
||||||
"message": "Adăugați o altă secțiune"
|
|
||||||
},
|
|
||||||
"sectionCode": {
|
|
||||||
"message": "Cod"
|
|
||||||
},
|
|
||||||
"sectionRemove": {
|
|
||||||
"message": "Ștergeți secțiunea"
|
|
||||||
},
|
|
||||||
"sectionRestore": {
|
|
||||||
"message": "Restaurează o secțiune ștearsă"
|
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"message": "Secțiuni"
|
|
||||||
},
|
|
||||||
"shortcutsNote": {
|
|
||||||
"message": "Creeați keyboard shortcuts"
|
|
||||||
},
|
|
||||||
"sortDateNewestFirst": {
|
|
||||||
"message": "cele mai noi la început"
|
|
||||||
},
|
|
||||||
"sortDateOldestFirst": {
|
|
||||||
"message": "cel mai vechi la început"
|
|
||||||
},
|
|
||||||
"sortLabel": {
|
|
||||||
"message": "Alegeți o sortare care să fie aplicată temelor instalate"
|
|
||||||
},
|
|
||||||
"sortLabelTitleAsc": {
|
|
||||||
"message": "Titlu crescător"
|
|
||||||
},
|
|
||||||
"sortLabelTitleDesc": {
|
|
||||||
"message": "Titlu descrescător"
|
|
||||||
},
|
|
||||||
"sortStylesHelp": {
|
|
||||||
"message": "Alegeți ce fel de sortare doriți să fie aplicată temelor instalate, din cadrul meniului dropdown. În mod implicit se aplică sortare alfabetică (A la Z) titlurilor temelor. Sortarea de tip \"Titlu descrescător\" va aplica sortare descrescătoare (Z la A) titlurilor.\nSunt alte presets care permit sortări după mai multe criterii. Este ca și cum ați sorta un tabel cu mai multe coloane iar fiecare categorie (între semnele +) reprezintă o coloană, sau un grup..\nSpre exemplu, dacă se alege \"Activată (prima) + Titlu\", atunci toate temele vor fi grupate astfel încât cele active sunt cap de listă, iar apoi temele sunt sortate alfabetic."
|
|
||||||
},
|
|
||||||
"sortStylesHelpTitle": {
|
|
||||||
"message": "Sortare conținut"
|
|
||||||
},
|
|
||||||
"styleBadRegexp": {
|
|
||||||
"message": "Regexp invalid."
|
|
||||||
},
|
|
||||||
"styleBeautify": {
|
|
||||||
"message": "Înfrumusețați"
|
|
||||||
},
|
|
||||||
"styleBeautifyIndentConditional": {
|
|
||||||
"message": "Indentare @media, @supports"
|
|
||||||
},
|
|
||||||
"styleBeautifyPreserveNewlines": {
|
|
||||||
"message": "Menține linii noi"
|
|
||||||
},
|
|
||||||
"styleCancelEditLabel": {
|
|
||||||
"message": "Înapoi la manager"
|
|
||||||
},
|
|
||||||
"styleChangesNotSaved": {
|
|
||||||
"message": "Ați modificat tema fără a o salva."
|
|
||||||
},
|
|
||||||
"styleEnabledLabel": {
|
|
||||||
"message": "Activat"
|
|
||||||
},
|
|
||||||
"styleFromMozillaFormatError": {
|
|
||||||
"message": "Importul din formatul Mozilla eșuat"
|
|
||||||
},
|
|
||||||
"styleFromMozillaFormatPrompt": {
|
|
||||||
"message": "Paste cod în format Mozilla"
|
|
||||||
},
|
|
||||||
"styleInstall": {
|
|
||||||
"message": "Instalați '$stylename$' în Stylus?",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleInstallFailed": {
|
|
||||||
"message": "Instalarea temei eșuată!\n$error$",
|
|
||||||
"placeholders": {
|
|
||||||
"error": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleInstallOverwrite": {
|
|
||||||
"message": "'$stylename$' este deja instalată. Scrieți peste?\nVersiune: $oldVersion$ -> $newVersion$",
|
|
||||||
"placeholders": {
|
|
||||||
"newVersion": {
|
|
||||||
"content": "$3"
|
|
||||||
},
|
|
||||||
"oldVersion": {
|
|
||||||
"content": "$2"
|
|
||||||
},
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleMissingName": {
|
|
||||||
"message": "Introduceți un nume"
|
|
||||||
},
|
|
||||||
"styleMozillaFormatHeading": {
|
|
||||||
"message": "Format Mozilla"
|
|
||||||
},
|
|
||||||
"styleNotAppliedRegexpProblemTooltip": {
|
|
||||||
"message": "Tema nu a fost aplicată din cauza incorectei aplicări a 'regexp()'"
|
|
||||||
},
|
|
||||||
"styleRegexpInvalidExplanation": {
|
|
||||||
"message": "Unele reguli 'regexp()' care nu au putut fi compilate."
|
|
||||||
},
|
|
||||||
"styleRegexpPartialExplanation": {
|
|
||||||
"message": "Această temă folosește regexp parțiale care încalcă <a href='https://developer.mozilla.org/docs/Web/CSS/@document'> specificațiile CSS4 @document</a> care are nevoie de o potrivire regexp exactă. Părțile afectate de CSS nu au fost aplicate paginii. Cel mai probabil tema a fost creată în Stylish-for-Chrome care verifică incorect 'regexp()' de la prima sa versiune (bug cunoscut)."
|
|
||||||
},
|
|
||||||
"styleRegexpProblemTooltip": {
|
|
||||||
"message": " Numărul de secțiuni care nu au fost aplicate din cauza utilizării incorecte a 'regexp()'"
|
|
||||||
},
|
|
||||||
"styleRegexpTestFull": {
|
|
||||||
"message": "Taburi găsite"
|
|
||||||
},
|
|
||||||
"styleRegexpTestInvalid": {
|
|
||||||
"message": "Invalid regexps au fost sărite"
|
|
||||||
},
|
|
||||||
"styleRegexpTestNone": {
|
|
||||||
"message": "Niciun tab nu se potrivește căutării"
|
|
||||||
},
|
|
||||||
"styleRegexpTestNote": {
|
|
||||||
"message": "Notă: folosiți \\ pentru ignorarea caracterelor speciale în regexp, care va fi transformat automat în \\\\ în codul temei, urmând specificațiile pentru text citat în CSS."
|
|
||||||
},
|
|
||||||
"styleRegexpTestPartial": {
|
|
||||||
"message": "Potrivire parțială, deci ignorată"
|
|
||||||
},
|
|
||||||
"styleRegexpTestTitle": {
|
|
||||||
"message": "Listă cu taburi deschise care se potrivesc căutării (click URL pentru a activa tabul)"
|
|
||||||
},
|
|
||||||
"styleSaveLabel": {
|
|
||||||
"message": "Salvați"
|
|
||||||
},
|
|
||||||
"styleToMozillaFormatHelp": {
|
|
||||||
"message": "Formatul Mozilla al codului poate fi uploadat pe userstyles.org și folosit de clasicul Stylish pentru Firefox."
|
|
||||||
},
|
|
||||||
"styleToMozillaFormatTitle": {
|
|
||||||
"message": "Scrieți în format Mozilla"
|
|
||||||
},
|
|
||||||
"styleUpdate": {
|
|
||||||
"message": "Sunteți sigur că doriți să faceți update la '$stylename$'?",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleUpdateDiscardChanges": {
|
|
||||||
"message": "Tema a fost modificată în afara editorului. Doriți să reâncărcați tema?"
|
|
||||||
},
|
|
||||||
"stylusUnavailableForURL": {
|
|
||||||
"message": "Stylus nu funcționează în astfel de pagini."
|
|
||||||
},
|
|
||||||
"stylusUnavailableForURLdetails": {
|
|
||||||
"message": "Ca o măsură de securitate, browser-ul împiedică extensiile să afecteze paginile corespunzătoare browser-ului (precum chrome://version, adresa unui nou tab începând cu Chrome 61, about:addons, în Firefox etc) și de asemenea paginile altor extensii. De asemenea este restricționat accesul la galeriile de extensii (Chrome Web Store sau AMO)."
|
|
||||||
},
|
|
||||||
"syncStorageErrorSaving": {
|
|
||||||
"message": "Valoarea nu a putut fi salvată. Încercați să reduceți dimensiunea textului."
|
|
||||||
},
|
|
||||||
"toggleStyle": {
|
|
||||||
"message": "Activați/Dezactivați tema"
|
|
||||||
},
|
|
||||||
"undoGlobal": {
|
|
||||||
"message": "Undo în toate secțiunile"
|
|
||||||
},
|
|
||||||
"unreachableAMO": {
|
|
||||||
"message": "Firefox împiedică accesul la site."
|
|
||||||
},
|
|
||||||
"unreachableAMOHint": {
|
|
||||||
"message": "Pentru a permite accesul deschideți <about:config>, right-click pe listă, click 'New', apoi 'Boolean', paste <privacy.resistFingerprinting.block_mozAddonManager> și click OK, <true>, OK, reâncărcați pagina <addons.mozilla.org>."
|
|
||||||
},
|
|
||||||
"unreachableContentScript": {
|
|
||||||
"message": "Nu s-a putut comunica cu pagina. Reîncărcați tabul."
|
|
||||||
},
|
|
||||||
"unreachableFileHint": {
|
|
||||||
"message": "Stylus poate accesa file:// URLs doar când este activată opțiunea respectivă din pagina cu setări chrome://extensions"
|
|
||||||
},
|
|
||||||
"unreachableMozSiteHintOldFF": {
|
|
||||||
"message": "Doar Firefox 59 sau mai nou poate fi configurat să permită WebExtension-urilor să adauge elemente la site-uri CSP-protected precum acesta."
|
|
||||||
},
|
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
|
||||||
"message": "Niciun update găsit."
|
|
||||||
},
|
|
||||||
"updateAllCheckSucceededSomeEdited": {
|
|
||||||
"message": "Unele teme care au update-uri nu au fost updatate pentru a nu pierde modificări locale. Update-urile pot fi forțate individual sau prin a reverifica update-urile pentru toate temele (modificările locale vor fi pierdute)."
|
|
||||||
},
|
|
||||||
"updateCheckFailBadResponseCode": {
|
|
||||||
"message": "Update eșuat: server-ul a răspuns cu acest cod $code$.",
|
|
||||||
"placeholders": {
|
|
||||||
"code": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"updateCheckFailServerUnreachable": {
|
|
||||||
"message": "Update eșuat: server-ul nu este avabil."
|
|
||||||
},
|
|
||||||
"updateCheckHistory": {
|
|
||||||
"message": "Historicul update-urilor"
|
|
||||||
},
|
|
||||||
"updateCheckManualUpdateForce": {
|
|
||||||
"message": "Instalați update-urile (modificările locale vor fi șterse)"
|
|
||||||
},
|
|
||||||
"updateCheckManualUpdateHint": {
|
|
||||||
"message": "Update-ul forțat va șterge toate modificările locale."
|
|
||||||
},
|
|
||||||
"updateCheckSkippedLocallyEdited": {
|
|
||||||
"message": "Tema a fost editată local."
|
|
||||||
},
|
|
||||||
"updateCheckSkippedMaybeLocallyEdited": {
|
|
||||||
"message": "Tema poate a fost modificată local."
|
|
||||||
},
|
|
||||||
"updateCheckSucceededNoUpdate": {
|
|
||||||
"message": "Tema este cu update-urile la zi."
|
|
||||||
},
|
|
||||||
"updateCompleted": {
|
|
||||||
"message": "Update realizat."
|
|
||||||
},
|
|
||||||
"updatesCurrentlyInstalled": {
|
|
||||||
"message": "Update-uri instalate:"
|
|
||||||
},
|
|
||||||
"usercssAvoidOverwriting": {
|
|
||||||
"message": "Vă rugăm să modificați valoarea @name și @namespace pentru a împiedica scrierea peste o temă existentă."
|
|
||||||
},
|
|
||||||
"usercssConfigIncomplete": {
|
|
||||||
"message": "Tema a fost updatată sau ștearsă după ce configurațiile au fost afișate. Aceste variabile nu au fost salvate pentru a nu corupe metadata temei."
|
|
||||||
},
|
|
||||||
"usercssEditorNamePlaceholder": {
|
|
||||||
"message": "Specificați @name"
|
|
||||||
},
|
|
||||||
"usercssReplaceTemplateConfirmation": {
|
|
||||||
"message": "Înlocuiți tema de bază a formatului Usercss cu acest cod?"
|
|
||||||
},
|
|
||||||
"usercssReplaceTemplateSectionBody": {
|
|
||||||
"message": "Introduce cod aici..."
|
|
||||||
},
|
|
||||||
"versionInvalidOlder": {
|
|
||||||
"message": "Versiunea este mai veche decât cea instalată."
|
|
||||||
},
|
|
||||||
"writeStyleFor": {
|
|
||||||
"message": "Scrieți temă pentru: "
|
|
||||||
},
|
|
||||||
"writeStyleForURL": {
|
|
||||||
"message": "acest URL"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,335 +1,416 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"appliesToEverything": {
|
||||||
"message": "Упиши нови стил"
|
"message": "Све",
|
||||||
},
|
"description": "Text displayed for styles that apply to all sites"
|
||||||
"addStyleTitle": {
|
},
|
||||||
"message": "Додај стил"
|
"linterIssues": {
|
||||||
},
|
"message": "Проблеми",
|
||||||
"appliesAdd": {
|
"description": "Label for the CSS linter issues block on the style edit page"
|
||||||
"message": "Додај"
|
},
|
||||||
},
|
"defaultTheme": {
|
||||||
"appliesDisplay": {
|
"message": "подразумевано",
|
||||||
"message": "Примењује се на: $applies$",
|
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||||
"placeholders": {
|
},
|
||||||
"applies": {
|
"exportLabel": {
|
||||||
"content": "$1"
|
"message": "Извези",
|
||||||
}
|
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||||
}
|
},
|
||||||
},
|
"cm_tabSize": {
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"message": "Величина картице",
|
||||||
"message": "и још"
|
"description": "Label for the text box controlling tab size option for the style editor."
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"enableStyleLabel": {
|
||||||
"message": "УРЛ адресе на домену"
|
"message": "Омогући",
|
||||||
},
|
"description": "Label for the button to enable a style"
|
||||||
"appliesHelp": {
|
},
|
||||||
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује."
|
"styleMissingName": {
|
||||||
},
|
"message": "Унесите назив",
|
||||||
"appliesLabel": {
|
"description": "Error displayed when user saves without providing a name"
|
||||||
"message": "Примењује се на"
|
},
|
||||||
},
|
"appliesDomainOption": {
|
||||||
"appliesRegexpOption": {
|
"message": "УРЛ адресе на домену",
|
||||||
"message": "УРЛ адресе које одговарају регуларном изразу"
|
"description": "Option to make the style apply to the entered string as a domain"
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"checkForUpdate": {
|
||||||
"message": "Уклони"
|
"message": "Проверите ажурирање",
|
||||||
},
|
"description": "Label for the button to check a single style for an update"
|
||||||
"appliesSpecify": {
|
},
|
||||||
"message": "Детаљније"
|
"importAppendLabel": {
|
||||||
},
|
"message": "Додај стилу",
|
||||||
"appliesToEverything": {
|
"description": "Label for the button to import a style and append to the existing sections"
|
||||||
"message": "Све"
|
},
|
||||||
},
|
"updateAllCheckSucceededNoUpdate": {
|
||||||
"appliesUrlOption": {
|
"message": "Сви стилови су ажурирани.",
|
||||||
"message": "УРЛ"
|
"description": "Text that displays when an update all check completed and no updates are available"
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"styleFromMozillaFormatPrompt": {
|
||||||
"message": "УРЛ адресе које почињу са"
|
"message": "Налепи код у Mozilla формату",
|
||||||
},
|
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||||
"applyAllUpdates": {
|
},
|
||||||
"message": "Примени сва ажурирања"
|
"helpAlt": {
|
||||||
},
|
"message": "Помоћ",
|
||||||
"checkAllUpdates": {
|
"description": "Alternate text for help buttons"
|
||||||
"message": "Проверите ажурирања за све стилове"
|
},
|
||||||
},
|
"search": {
|
||||||
"checkForUpdate": {
|
"message": "Претражи",
|
||||||
"message": "Проверите ажурирање"
|
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"confirmYes": {
|
||||||
"message": "Проверавање..."
|
"message": "Да",
|
||||||
},
|
"description": "'Yes' button in a confirm dialog"
|
||||||
"cm_indentWithTabs": {
|
},
|
||||||
"message": "Користи картице са паметним увлачењем редова"
|
"findStylesForSite": {
|
||||||
},
|
"message": "Пронађи још стилова за овај сајт",
|
||||||
"cm_keyMap": {
|
"description": "Text for a link that gets a list of styles for the current site"
|
||||||
"message": "Мапа тастера"
|
},
|
||||||
},
|
"manageHeading": {
|
||||||
"cm_lineWrapping": {
|
"message": "Инсталирани стилови",
|
||||||
"message": "Преламање текста"
|
"description": "Heading for the manage page"
|
||||||
},
|
},
|
||||||
"cm_smartIndent": {
|
"styleBeautify": {
|
||||||
"message": "Користи паметно увлачење редова"
|
"message": "Улепшај",
|
||||||
},
|
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||||
"cm_tabSize": {
|
},
|
||||||
"message": "Величина картице"
|
"styleEnabledLabel": {
|
||||||
},
|
"message": "Омогућено",
|
||||||
"cm_theme": {
|
"description": "Label for the enabled state of styles"
|
||||||
"message": "Тема"
|
},
|
||||||
},
|
"styleToMozillaFormatHelp": {
|
||||||
"confirmDelete": {
|
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.",
|
||||||
"message": "Избриши"
|
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||||
},
|
},
|
||||||
"confirmNo": {
|
"sectionAdd": {
|
||||||
"message": "Не"
|
"message": "Додај нови одељак",
|
||||||
},
|
"description": "Label for the button to add a section"
|
||||||
"confirmSave": {
|
},
|
||||||
"message": "Сачувај"
|
"styleSaveLabel": {
|
||||||
},
|
"message": "Сачувај",
|
||||||
"confirmStop": {
|
"description": "Label for save button for style editing"
|
||||||
"message": "Заустави"
|
},
|
||||||
},
|
"confirmStop": {
|
||||||
"confirmYes": {
|
"message": "Заустави",
|
||||||
"message": "Да"
|
"description": "'Stop' button in a confirm dialog"
|
||||||
},
|
},
|
||||||
"dbError": {
|
"writeStyleForURL": {
|
||||||
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?"
|
"message": "ову УРЛ адресу",
|
||||||
},
|
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||||
"defaultTheme": {
|
},
|
||||||
"message": "подразумевано"
|
"appliesAdd": {
|
||||||
},
|
"message": "Додај",
|
||||||
"deleteStyleConfirm": {
|
"description": "Label for the button to add an 'applies' entry"
|
||||||
"message": "Да ли сте сигурни да желите да избришете овај стил?"
|
},
|
||||||
},
|
"appliesRegexpOption": {
|
||||||
"deleteStyleLabel": {
|
"message": "УРЛ адресе које одговарају регуларном изразу",
|
||||||
"message": "Избриши"
|
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||||
},
|
},
|
||||||
"description": {
|
"styleInstall": {
|
||||||
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове."
|
"message": "Инсталирати '$stylename$' у Stylus?",
|
||||||
},
|
"description": "Confirmation when installing a style",
|
||||||
"disableAllStyles": {
|
"placeholders": {
|
||||||
"message": "Искључи све стилове"
|
"stylename": {
|
||||||
},
|
"content": "$1"
|
||||||
"disableStyleLabel": {
|
}
|
||||||
"message": "Онемогући"
|
}
|
||||||
},
|
},
|
||||||
"editDeleteText": {
|
"manageText": {
|
||||||
"message": "Избриши"
|
"message": "<a href='https://userstyles.org'>Преузмите стилове са userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Помоћ</a>",
|
||||||
},
|
"description": "Help text on the manage page"
|
||||||
"editGotoLine": {
|
},
|
||||||
"message": "Иди на ред (или line:col)"
|
"linterIssuesHelp": {
|
||||||
},
|
"message": "Проблем пронађен од стране $link$:",
|
||||||
"editStyleHeading": {
|
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
||||||
"message": "Уреди стил"
|
"placeholders": {
|
||||||
},
|
"link": {
|
||||||
"editStyleLabel": {
|
"content": "$1"
|
||||||
"message": "Уреди"
|
}
|
||||||
},
|
}
|
||||||
"editStyleTitle": {
|
},
|
||||||
"message": "Уреди стил $stylename$",
|
"searchStyles": {
|
||||||
"placeholders": {
|
"message": "Претражи садржај",
|
||||||
"stylename": {
|
"description": "Label for the search filter textbox on the Manage styles page"
|
||||||
"content": "$1"
|
},
|
||||||
}
|
"disableStyleLabel": {
|
||||||
}
|
"message": "Онемогући",
|
||||||
},
|
"description": "Label for the button to disable a style"
|
||||||
"enableStyleLabel": {
|
},
|
||||||
"message": "Омогући"
|
"prefShowBadge": {
|
||||||
},
|
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци",
|
||||||
"exportLabel": {
|
"description": "Label for the checkbox controlling toolbar badge text."
|
||||||
"message": "Извези"
|
},
|
||||||
},
|
"menuShowBadge": {
|
||||||
"genericAdd": {
|
"message": "Прикажи број активних стилова",
|
||||||
"message": "Додај"
|
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||||
},
|
},
|
||||||
"genericEnabledLabel": {
|
"cm_lineWrapping": {
|
||||||
"message": "Омогућено"
|
"message": "Преламање текста",
|
||||||
},
|
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||||
"helpAlt": {
|
},
|
||||||
"message": "Помоћ"
|
"styleCancelEditLabel": {
|
||||||
},
|
"message": "Назад на управљање",
|
||||||
"helpKeyMapCommand": {
|
"description": "Label for cancel button for style editing"
|
||||||
"message": "Укуцај име команде"
|
},
|
||||||
},
|
"styleChangesNotSaved": {
|
||||||
"helpKeyMapHotkey": {
|
"message": "Направили сте измене овог стила које нисте сачували.",
|
||||||
"message": "Притисни пречицу"
|
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||||
},
|
},
|
||||||
"importAppendLabel": {
|
"importLabel": {
|
||||||
"message": "Додај стилу"
|
"message": "Увези",
|
||||||
},
|
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||||
"importAppendTooltip": {
|
},
|
||||||
"message": "Додај увезени стил тренутном стилу"
|
"updateCheckFailServerUnreachable": {
|
||||||
},
|
"message": "Ажурирање није успело: сервер није доступан.",
|
||||||
"importLabel": {
|
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||||
"message": "Увези"
|
},
|
||||||
},
|
"manageFilters": {
|
||||||
"importReplaceLabel": {
|
"message": "Филтери",
|
||||||
"message": "Упиши преко стила"
|
"description": "Label for filters container"
|
||||||
},
|
},
|
||||||
"importReplaceTooltip": {
|
"applyAllUpdates": {
|
||||||
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил"
|
"message": "Примени сва ажурирања",
|
||||||
},
|
"description": "Label for the button to apply all detected updates"
|
||||||
"installUpdate": {
|
},
|
||||||
"message": "Инсталирај ажурирање"
|
"deleteStyleConfirm": {
|
||||||
},
|
"message": "Да ли сте сигурни да желите да избришете овај стил?",
|
||||||
"linkGetHelp": {
|
"description": "Confirmation before deleting a style"
|
||||||
"message": "Помоћ"
|
},
|
||||||
},
|
"styleBadRegexp": {
|
||||||
"linkGetStyles": {
|
"message": "Регуларни израз је неисправан.",
|
||||||
"message": "Преузмите стилове"
|
"description": "Validation message for a bad regexp in a style"
|
||||||
},
|
},
|
||||||
"linterIssues": {
|
"optionsHeading": {
|
||||||
"message": "Проблеми"
|
"message": "Опције",
|
||||||
},
|
"description": "Heading for options section on manage page."
|
||||||
"linterIssuesHelp": {
|
},
|
||||||
"message": "Проблем пронађен од стране $link$:",
|
"appliesDisplay": {
|
||||||
"placeholders": {
|
"message": "Примењује се на: $applies$",
|
||||||
"link": {
|
"description": "Text on the manage screen to describe what the style applies to",
|
||||||
"content": "$1"
|
"placeholders": {
|
||||||
}
|
"applies": {
|
||||||
}
|
"content": "$1"
|
||||||
},
|
}
|
||||||
"manageFilters": {
|
}
|
||||||
"message": "Филтери"
|
},
|
||||||
},
|
"styleUpdate": {
|
||||||
"manageHeading": {
|
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
|
||||||
"message": "Инсталирани стилови"
|
"description": "Confirmation when updating a style",
|
||||||
},
|
"placeholders": {
|
||||||
"manageOnlyEnabled": {
|
"stylename": {
|
||||||
"message": "Само омогућени стилови"
|
"content": "$1"
|
||||||
},
|
}
|
||||||
"menuShowBadge": {
|
}
|
||||||
"message": "Прикажи број активних стилова"
|
},
|
||||||
},
|
"styleSectionsTitle": {
|
||||||
"noStylesForSite": {
|
"message": "Одељци",
|
||||||
"message": "Нема инсталираних стилова за овај сајт."
|
"description": "Title for the style sections section"
|
||||||
},
|
},
|
||||||
"openManage": {
|
"editStyleTitle": {
|
||||||
"message": "Управљај инсталираним стиловима"
|
"message": "Уреди стил $stylename$",
|
||||||
},
|
"description": "Title of the page for editing styles",
|
||||||
"openOptions": {
|
"placeholders": {
|
||||||
"message": "Опције"
|
"stylename": {
|
||||||
},
|
"content": "$1"
|
||||||
"optionsHeading": {
|
}
|
||||||
"message": "Опције"
|
}
|
||||||
},
|
},
|
||||||
"optionsSyncUrl": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "УРЛ"
|
"message": "Стил је ажуриран.",
|
||||||
},
|
"description": "Text that displays when an update check completed and no update is available"
|
||||||
"popupStylesFirst": {
|
},
|
||||||
"message": "Излистај стилове пре команди у менију дугмета на алатној траци"
|
"appliesUrlPrefixOption": {
|
||||||
},
|
"message": "УРЛ адресе које почињу са",
|
||||||
"prefShowBadge": {
|
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||||
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци"
|
},
|
||||||
},
|
"searchRegexp": {
|
||||||
"replace": {
|
"message": "Користи /re/ синтаксу за претрагу регуларним изразом",
|
||||||
"message": "Замени"
|
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||||
},
|
},
|
||||||
"replaceAll": {
|
"importReplaceTooltip": {
|
||||||
"message": "Замени све"
|
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил",
|
||||||
},
|
"description": "Label for the button to import and overwrite current style"
|
||||||
"replaceWith": {
|
},
|
||||||
"message": "Замени са"
|
"popupStylesFirst": {
|
||||||
},
|
"message": "Излистај стилове пре команди у менију дугмета на алатној траци",
|
||||||
"search": {
|
"description": "Label for the checkbox controlling section order in the popup."
|
||||||
"message": "Претражи"
|
},
|
||||||
},
|
"sectionHelp": {
|
||||||
"searchRegexp": {
|
"message": "Одељци вам омогућавају да дефинишете различите делове кода који се примењују на раличите скупове УРЛ-ова у истом стилу. На пример, један исти стил може променити почетну страницу једног сајта на један начин а остатак сајта на други начин.",
|
||||||
"message": "Користи /re/ синтаксу за претрагу регуларним изразом"
|
"description": "Help text for sections"
|
||||||
},
|
},
|
||||||
"sectionAdd": {
|
"noStylesForSite": {
|
||||||
"message": "Додај нови одељак"
|
"message": "Нема инсталираних стилова за овај сајт.",
|
||||||
},
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
"sectionCode": {
|
},
|
||||||
"message": "Код"
|
"appliesDisplayTruncatedSuffix": {
|
||||||
},
|
"message": "и још",
|
||||||
"sectionRemove": {
|
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||||
"message": "Уклони одељак"
|
},
|
||||||
},
|
"appliesRemove": {
|
||||||
"sections": {
|
"message": "Уклони",
|
||||||
"message": "Одељци"
|
"description": "Label for the button to remove an 'applies' entry"
|
||||||
},
|
},
|
||||||
"styleBadRegexp": {
|
"styleToMozillaFormatTitle": {
|
||||||
"message": "Регуларни израз је неисправан."
|
"message": "Стил у Mozilla формату",
|
||||||
},
|
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
|
||||||
"styleBeautify": {
|
},
|
||||||
"message": "Улепшај"
|
"writeStyleFor": {
|
||||||
},
|
"message": "Упиши стил за:",
|
||||||
"styleCancelEditLabel": {
|
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||||
"message": "Назад на управљање"
|
},
|
||||||
},
|
"replace": {
|
||||||
"styleChangesNotSaved": {
|
"message": "Замени",
|
||||||
"message": "Направили сте измене овог стила које нисте сачували."
|
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||||
},
|
},
|
||||||
"styleEnabledLabel": {
|
"appliesLabel": {
|
||||||
"message": "Омогућено"
|
"message": "Примењује се на",
|
||||||
},
|
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||||
"styleFromMozillaFormatPrompt": {
|
},
|
||||||
"message": "Налепи код у Mozilla формату"
|
"openManage": {
|
||||||
},
|
"message": "Управљај инсталираним стиловима",
|
||||||
"styleInstall": {
|
"description": "Link to open the manage page."
|
||||||
"message": "Инсталирати '$stylename$' у Stylus?",
|
},
|
||||||
"placeholders": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"stylename": {
|
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
|
||||||
"content": "$1"
|
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||||
}
|
"placeholders": {
|
||||||
}
|
"code": {
|
||||||
},
|
"content": "$1"
|
||||||
"styleMissingName": {
|
}
|
||||||
"message": "Унесите назив"
|
}
|
||||||
},
|
},
|
||||||
"styleMozillaFormatHeading": {
|
"appliesSpecify": {
|
||||||
"message": "Mozilla формат"
|
"message": "Детаљније",
|
||||||
},
|
"description": "Label for the button to make a style apply only to specific sites"
|
||||||
"styleSaveLabel": {
|
},
|
||||||
"message": "Сачувај"
|
"installUpdate": {
|
||||||
},
|
"message": "Инсталирај ажурирање",
|
||||||
"styleToMozillaFormatHelp": {
|
"description": "Label for the button to install an update for a single style"
|
||||||
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org."
|
},
|
||||||
},
|
"styleMozillaFormatHeading": {
|
||||||
"styleToMozillaFormatTitle": {
|
"message": "Mozilla формат",
|
||||||
"message": "Стил у Mozilla формату"
|
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||||
},
|
},
|
||||||
"styleUpdate": {
|
"sectionRemove": {
|
||||||
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
|
"message": "Уклони одељак",
|
||||||
"placeholders": {
|
"description": "Label for the button to remove a section"
|
||||||
"stylename": {
|
},
|
||||||
"content": "$1"
|
"disableAllStyles": {
|
||||||
}
|
"message": "Искључи све стилове",
|
||||||
}
|
"description": "Label for the checkbox that turns all enabled styles off."
|
||||||
},
|
},
|
||||||
"stylusUnavailableForURL": {
|
"undoGlobal": {
|
||||||
"message": "Stylus не ради на страницама као што је ова."
|
"message": "Опозови (свеобухватно)",
|
||||||
},
|
"description": "CSS-beautify global Undo button label"
|
||||||
"undo": {
|
},
|
||||||
"message": "Опозови"
|
"updateCompleted": {
|
||||||
},
|
"message": "Ажурирање је комплетирано.",
|
||||||
"undoGlobal": {
|
"description": "Text that displays when an update completed"
|
||||||
"message": "Опозови (свеобухватно)"
|
},
|
||||||
},
|
"checkingForUpdate": {
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
"message": "Проверавање...",
|
||||||
"message": "Сви стилови су ажурирани."
|
"description": "Text to display when checking a style for an update"
|
||||||
},
|
},
|
||||||
"updateCheckFailBadResponseCode": {
|
"sectionCode": {
|
||||||
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
|
"message": "Код",
|
||||||
"placeholders": {
|
"description": "Label for the code for a section"
|
||||||
"code": {
|
},
|
||||||
"content": "$1"
|
"cm_smartIndent": {
|
||||||
}
|
"message": "Користи паметно увлачење редова",
|
||||||
}
|
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||||
},
|
},
|
||||||
"updateCheckFailServerUnreachable": {
|
"appliesHelp": {
|
||||||
"message": "Ажурирање није успело: сервер није доступан."
|
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.",
|
||||||
},
|
"description": "Help text for 'applies to' section"
|
||||||
"updateCheckSucceededNoUpdate": {
|
},
|
||||||
"message": "Стил је ажуриран."
|
"editStyleHeading": {
|
||||||
},
|
"message": "Уреди стил",
|
||||||
"updateCompleted": {
|
"description": "Title of the page for editing styles"
|
||||||
"message": "Ажурирање је комплетирано."
|
},
|
||||||
},
|
"appliesUrlOption": {
|
||||||
"writeStyleFor": {
|
"message": "УРЛ",
|
||||||
"message": "Упиши стил за:"
|
"description": "Option to make the style apply to the entered string as a URL"
|
||||||
},
|
},
|
||||||
"writeStyleForURL": {
|
"stylusUnavailableForURL": {
|
||||||
"message": "ову УРЛ адресу"
|
"message": "Stylus не ради на страницама као што је ова.",
|
||||||
}
|
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||||
}
|
},
|
||||||
|
"addStyleTitle": {
|
||||||
|
"message": "Додај стил",
|
||||||
|
"description": "Title of the page for adding styles"
|
||||||
|
},
|
||||||
|
"importReplaceLabel": {
|
||||||
|
"message": "Упиши преко стила",
|
||||||
|
"description": "Label for the button to import and overwrite current style"
|
||||||
|
},
|
||||||
|
"dbError": {
|
||||||
|
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?",
|
||||||
|
"description": "Prompt when a DB error is encountered"
|
||||||
|
},
|
||||||
|
"importAppendTooltip": {
|
||||||
|
"message": "Додај увезени стил тренутном стилу",
|
||||||
|
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||||
|
},
|
||||||
|
"helpKeyMapHotkey": {
|
||||||
|
"message": "Притисни пречицу",
|
||||||
|
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||||
|
},
|
||||||
|
"replaceAll": {
|
||||||
|
"message": "Замени све",
|
||||||
|
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||||
|
},
|
||||||
|
"editGotoLine": {
|
||||||
|
"message": "Иди на ред (или line:col)",
|
||||||
|
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||||
|
},
|
||||||
|
"checkAllUpdates": {
|
||||||
|
"message": "Проверите ажурирања за све стилове",
|
||||||
|
"description": "Label for the button to check all styles for updates"
|
||||||
|
},
|
||||||
|
"confirmNo": {
|
||||||
|
"message": "Не",
|
||||||
|
"description": "'No' button in a confirm dialog"
|
||||||
|
},
|
||||||
|
"undo": {
|
||||||
|
"message": "Опозови",
|
||||||
|
"description": "Button label"
|
||||||
|
},
|
||||||
|
"cm_keyMap": {
|
||||||
|
"message": "Мапа тастера",
|
||||||
|
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||||
|
},
|
||||||
|
"cm_indentWithTabs": {
|
||||||
|
"message": "Користи картице са паметним увлачењем редова",
|
||||||
|
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||||
|
},
|
||||||
|
"replaceWith": {
|
||||||
|
"message": "Замени са",
|
||||||
|
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||||
|
},
|
||||||
|
"deleteStyleLabel": {
|
||||||
|
"message": "Избриши",
|
||||||
|
"description": "Label for the button to delete a style"
|
||||||
|
},
|
||||||
|
"addStyleLabel": {
|
||||||
|
"message": "Упиши нови стил",
|
||||||
|
"description": "Label for the button to go to the add style page"
|
||||||
|
},
|
||||||
|
"manageOnlyEnabled": {
|
||||||
|
"message": "Само омогућени стилови",
|
||||||
|
"description": "Checkbox to show only enabled styles"
|
||||||
|
},
|
||||||
|
"editStyleLabel": {
|
||||||
|
"message": "Уреди",
|
||||||
|
"description": "Label for the button to go to the edit style page"
|
||||||
|
},
|
||||||
|
"cm_theme": {
|
||||||
|
"message": "Тема",
|
||||||
|
"description": "Label for the style editor's CSS theme."
|
||||||
|
},
|
||||||
|
"helpKeyMapCommand": {
|
||||||
|
"message": "Укуцај име команде",
|
||||||
|
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.",
|
||||||
|
"description": "Extension description"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,67 +1,71 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"appliesToEverything": {
|
||||||
"message": "క్రొత్త స్టైల్ వ్రాయండి"
|
"message": "అన్నిటికీ",
|
||||||
},
|
"description": "Text displayed for styles that apply to all sites"
|
||||||
"appliesAdd": {
|
},
|
||||||
"message": "చేర్చు"
|
"enableStyleLabel": {
|
||||||
},
|
"message": "చేతనించు",
|
||||||
"appliesDisplay": {
|
"description": "Label for the button to enable a style"
|
||||||
"message": "వేటికి వర్తిస్తుంది; $applies$",
|
},
|
||||||
"placeholders": {
|
"helpAlt": {
|
||||||
"applies": {
|
"message": "సహాయం",
|
||||||
"content": "$1"
|
"description": "Alternate text for help buttons"
|
||||||
}
|
},
|
||||||
}
|
"manageHeading": {
|
||||||
},
|
"message": "స్థాపిత శైలులు",
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"description": "Heading for the manage page"
|
||||||
"message": "ఇంకా మరిన్ని"
|
},
|
||||||
},
|
"styleSaveLabel": {
|
||||||
"appliesRemove": {
|
"message": "భద్రపరచు",
|
||||||
"message": "తొలగించు"
|
"description": "Label for save button for style editing"
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesAdd": {
|
||||||
"message": "అన్నిటికీ"
|
"message": "చేర్చు",
|
||||||
},
|
"description": "Label for the button to add an 'applies' entry"
|
||||||
"confirmDelete": {
|
},
|
||||||
"message": "తొలగించు"
|
"disableStyleLabel": {
|
||||||
},
|
"message": "అచేతనించు",
|
||||||
"confirmSave": {
|
"description": "Label for the button to disable a style"
|
||||||
"message": "భద్రపరచు"
|
},
|
||||||
},
|
"deleteStyleConfirm": {
|
||||||
"deleteStyleConfirm": {
|
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
|
||||||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?"
|
"description": "Confirmation before deleting a style"
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"appliesDisplay": {
|
||||||
"message": "తొలగించు"
|
"message": "వేటికి వర్తిస్తుంది; $applies$",
|
||||||
},
|
"description": "Text on the manage screen to describe what the style applies to",
|
||||||
"disableStyleLabel": {
|
"placeholders": {
|
||||||
"message": "అచేతనించు"
|
"applies": {
|
||||||
},
|
"content": "$1"
|
||||||
"editDeleteText": {
|
}
|
||||||
"message": "తొలగించు"
|
}
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"styleSectionsTitle": {
|
||||||
"message": "మార్చు"
|
"message": "విభాగాలు",
|
||||||
},
|
"description": "Title for the style sections section"
|
||||||
"enableStyleLabel": {
|
},
|
||||||
"message": "చేతనించు"
|
"appliesDisplayTruncatedSuffix": {
|
||||||
},
|
"message": "ఇంకా మరిన్ని",
|
||||||
"genericAdd": {
|
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||||
"message": "చేర్చు"
|
},
|
||||||
},
|
"appliesRemove": {
|
||||||
"helpAlt": {
|
"message": "తొలగించు",
|
||||||
"message": "సహాయం"
|
"description": "Label for the button to remove an 'applies' entry"
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageTitle": {
|
||||||
"message": "స్థాపిత శైలులు"
|
"message": "స్టైలిష్",
|
||||||
},
|
"description": "Title for the manage page"
|
||||||
"manageTitle": {
|
},
|
||||||
"message": "స్టైలిష్"
|
"deleteStyleLabel": {
|
||||||
},
|
"message": "తొలగించు",
|
||||||
"sections": {
|
"description": "Label for the button to delete a style"
|
||||||
"message": "విభాగాలు"
|
},
|
||||||
},
|
"addStyleLabel": {
|
||||||
"styleSaveLabel": {
|
"message": "క్రొత్త స్టైల్ వ్రాయండి",
|
||||||
"message": "భద్రపరచు"
|
"description": "Label for the button to go to the add style page"
|
||||||
}
|
},
|
||||||
}
|
"editStyleLabel": {
|
||||||
|
"message": "మార్చు",
|
||||||
|
"description": "Label for the button to go to the edit style page"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,791 +1,206 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"appliesToEverything": {
|
||||||
"message": "Yeni stil oluşturun"
|
"message": "Her şey",
|
||||||
},
|
"description": "Text displayed for styles that apply to all sites"
|
||||||
"addStyleTitle": {
|
},
|
||||||
"message": "Stil Ekleyin"
|
"enableStyleLabel": {
|
||||||
},
|
"message": "Etkinleştir",
|
||||||
"alphaChannel": {
|
"description": "Label for the button to enable a style"
|
||||||
"message": "Opaklık"
|
},
|
||||||
},
|
"styleMissingName": {
|
||||||
"appliesAdd": {
|
"message": "Bir ad girin",
|
||||||
"message": "Ekleyin"
|
"description": "Error displayed when user saves without providing a name"
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDomainOption": {
|
||||||
"message": "Şuraya uygulanır: $applies$",
|
"message": "Alan adındaki URLler",
|
||||||
"placeholders": {
|
"description": "Option to make the style apply to the entered string as a domain"
|
||||||
"applies": {
|
},
|
||||||
"content": "$1"
|
"checkForUpdate": {
|
||||||
}
|
"message": "Güncellemeleri denetle",
|
||||||
}
|
"description": "Label for the button to check a single style for an update"
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"helpAlt": {
|
||||||
"message": "ve diğerleri"
|
"message": "Yardım",
|
||||||
},
|
"description": "Alternate text for help buttons"
|
||||||
"appliesDomainOption": {
|
},
|
||||||
"message": "Alan adındaki URLler"
|
"findStylesForSite": {
|
||||||
},
|
"message": "Bu site için başka stiller bul",
|
||||||
"appliesHelp": {
|
"description": "Text for a link that gets a list of styles for the current site"
|
||||||
"message": "Bu bölümdeki kodun hangi URLlere uygulanacağını sınırlamak için 'Şuraya uygulanır' denetimlerini kullanın."
|
},
|
||||||
},
|
"manageHeading": {
|
||||||
"appliesLabel": {
|
"message": "Yüklü Stiller",
|
||||||
"message": "Şuraya uygulanır"
|
"description": "Heading for the manage page"
|
||||||
},
|
},
|
||||||
"appliesLineWidgetWarning": {
|
"styleEnabledLabel": {
|
||||||
"message": "Küçültülmüş CSS ile çalışmaz"
|
"message": "Etkin",
|
||||||
},
|
"description": "Label for the enabled state of styles"
|
||||||
"appliesRegexpOption": {
|
},
|
||||||
"message": "regexp ile eşleşen URL'ler"
|
"styleToMozillaFormatHelp": {
|
||||||
},
|
"message": "Kodun Mozilla biçimi, Firefox için Stylish ile kullanılabilir ve userstyles.org sitesine gönderilebilir.",
|
||||||
"appliesRemove": {
|
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||||
"message": "Kaldır"
|
},
|
||||||
},
|
"sectionAdd": {
|
||||||
"appliesSpecify": {
|
"message": "Başka bölüm ekle",
|
||||||
"message": "Belirt"
|
"description": "Label for the button to add a section"
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"styleSaveLabel": {
|
||||||
"message": "Her şey"
|
"message": "Kaydet",
|
||||||
},
|
"description": "Label for save button for style editing"
|
||||||
"appliesUrlPrefixOption": {
|
},
|
||||||
"message": "Şununla başlayan URL'ler:"
|
"appliesAdd": {
|
||||||
},
|
"message": "Ekleyin",
|
||||||
"applyAllUpdates": {
|
"description": "Label for the button to add an 'applies' entry"
|
||||||
"message": "Tüm güncellemeleri uygula"
|
},
|
||||||
},
|
"appliesRegexpOption": {
|
||||||
"author": {
|
"message": "regexp ile eşleşen URL'ler",
|
||||||
"message": "Yazar"
|
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||||
},
|
},
|
||||||
"backupButtons": {
|
"styleInstall": {
|
||||||
"message": "Yedek"
|
"message": "'$stylename$' Stylus'e yüklensin mi?",
|
||||||
},
|
"description": "Confirmation when installing a style",
|
||||||
"bckpInstStyles": {
|
"placeholders": {
|
||||||
"message": "Dışa aktar"
|
"stylename": {
|
||||||
},
|
"content": "$1"
|
||||||
"checkAllUpdates": {
|
}
|
||||||
"message": "Tüm stiller için güncellemeleri denetle"
|
}
|
||||||
},
|
},
|
||||||
"checkAllUpdatesForce": {
|
"disableStyleLabel": {
|
||||||
"message": "Tekrar kontrol et, herhangi bir stil düzenlemedim!"
|
"message": "Devre dışı bırak",
|
||||||
},
|
"description": "Label for the button to disable a style"
|
||||||
"checkForUpdate": {
|
},
|
||||||
"message": "Güncellemeleri denetle"
|
"styleCancelEditLabel": {
|
||||||
},
|
"message": "Yönetim sayfasına dön",
|
||||||
"checkingForUpdate": {
|
"description": "Label for cancel button for style editing"
|
||||||
"message": "Kontrol ediliyor..."
|
},
|
||||||
},
|
"styleChangesNotSaved": {
|
||||||
"clickToUninstall": {
|
"message": "Bu stilde yaptığınız, kaydedilmemiş değişiklikler var.",
|
||||||
"message": "Kaldırmak için tıklayın"
|
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||||
},
|
},
|
||||||
"cm_autoCloseBrackets": {
|
"updateCheckFailServerUnreachable": {
|
||||||
"message": "Parantezleri ve tırnak işaretlerini otomatik olarak kapat"
|
"message": "Güncellenemedi: sunucuya erişilemiyor.",
|
||||||
},
|
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||||
"cm_autoCloseBracketsTooltip": {
|
},
|
||||||
"message": "()[]{}''\"\" Açılışlarından birini yazarken otomatik olarak bir kapanış çifti ekleyin"
|
"deleteStyleConfirm": {
|
||||||
},
|
"message": "Bu stili silmek istediğinizden emin misiniz?",
|
||||||
"cm_autocompleteOnTyping": {
|
"description": "Confirmation before deleting a style"
|
||||||
"message": "Yazarken tamamla"
|
},
|
||||||
},
|
"appliesDisplay": {
|
||||||
"cm_colorpicker": {
|
"message": "Şuraya uygulanır: $applies$",
|
||||||
"message": "CSS renk seçicileri"
|
"description": "Text on the manage screen to describe what the style applies to",
|
||||||
},
|
"placeholders": {
|
||||||
"cm_indentWithTabs": {
|
"applies": {
|
||||||
"message": "Akıllı girintili tab kullan"
|
"content": "$1"
|
||||||
},
|
}
|
||||||
"cm_keyMap": {
|
}
|
||||||
"message": "Tuşeşlem"
|
},
|
||||||
},
|
"styleSectionsTitle": {
|
||||||
"cm_lineWrapping": {
|
"message": "Bölümler",
|
||||||
"message": "Kelime kaydırma"
|
"description": "Title for the style sections section"
|
||||||
},
|
},
|
||||||
"cm_matchHighlight": {
|
"editStyleTitle": {
|
||||||
"message": "Vurgulama"
|
"message": "$stylename$ Stilini Düzenleyin",
|
||||||
},
|
"description": "Title of the page for editing styles",
|
||||||
"cm_matchHighlightSelection": {
|
"placeholders": {
|
||||||
"message": "Yalnızca seçim"
|
"stylename": {
|
||||||
},
|
"content": "$1"
|
||||||
"cm_matchHighlightToken": {
|
}
|
||||||
"message": "İmleç altındaki token"
|
}
|
||||||
},
|
},
|
||||||
"cm_resizeGripHint": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "Yüksekliği en üst düzeye çıkarmak/geri yüklemek için çift tıklayın"
|
"message": "Stil güncel.",
|
||||||
},
|
"description": "Text that displays when an update check completed and no update is available"
|
||||||
"cm_selectByTokens": {
|
},
|
||||||
"message": "Çift tıklamak tokenleri seçer"
|
"appliesUrlPrefixOption": {
|
||||||
},
|
"message": "Şununla başlayan URL'ler:",
|
||||||
"cm_selectByTokensTooltip": {
|
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||||
"message": "Token örnekleri: .foo-bar-2 #aabbcc 0.32 !important\nDevre dışı bırakıldığında: noktalama işareti ile ayrılmış kelimeler seçilir."
|
},
|
||||||
},
|
"sectionHelp": {
|
||||||
"cm_smartIndent": {
|
"message": "Bölümler, aynı stil içindeki farklı URL kümelerine uygulanacak farklı kod parçaları tanımlamanıza olanak sağlar. Örneğin, tek bir stil bir sitenin ana sayfasını belirli bir şekilde değiştirirken, sayfanın kalan kısmını farklı bir şekilde değiştirebilir.",
|
||||||
"message": "Akıllı girinti kullanma"
|
"description": "Help text for sections"
|
||||||
},
|
},
|
||||||
"cm_tabSize": {
|
"noStylesForSite": {
|
||||||
"message": "Tab büyüklüğü"
|
"message": "Bu site için hiçbir stil yüklenmedi.",
|
||||||
},
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
"cm_theme": {
|
},
|
||||||
"message": "Tema"
|
"appliesDisplayTruncatedSuffix": {
|
||||||
},
|
"message": "ve diğerleri",
|
||||||
"colorpickerSwitchFormatTooltip": {
|
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||||
"message": "Format değişimleri: HEX -> RGB -> HSL.\nYönü ters çevirmek için Shift tuşunu basılı tutup tıklatın.\nAyrıca PgUp (PageUp), PgDn (PageDown) tuşları ile."
|
},
|
||||||
},
|
"appliesRemove": {
|
||||||
"colorpickerTooltip": {
|
"message": "Kaldır",
|
||||||
"message": "Renk seçiciyi aç"
|
"description": "Label for the button to remove an 'applies' entry"
|
||||||
},
|
},
|
||||||
"configOnChange": {
|
"appliesLabel": {
|
||||||
"message": "değişiklikte"
|
"message": "Şuraya uygulanır",
|
||||||
},
|
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||||
"configOnChangeTooltip": {
|
},
|
||||||
"message": "Otomatik kaydetme ve değişiklikleri otomatik olarak uygulama"
|
"openManage": {
|
||||||
},
|
"message": "Yüklü stilleri yönet",
|
||||||
"configureStyle": {
|
"description": "Link to open the manage page."
|
||||||
"message": "Yapılandır"
|
},
|
||||||
},
|
"updateCheckFailBadResponseCode": {
|
||||||
"configureStyleOnHomepage": {
|
"message": "Güncellenemedi: sunucu yanıt olarak $code$ kodunu gönderdi.",
|
||||||
"message": "Ana sayfada yapılandır"
|
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||||
},
|
"placeholders": {
|
||||||
"confirmCancel": {
|
"code": {
|
||||||
"message": "İptal"
|
"content": "$1"
|
||||||
},
|
}
|
||||||
"confirmClose": {
|
}
|
||||||
"message": "Kapat"
|
},
|
||||||
},
|
"appliesSpecify": {
|
||||||
"confirmDefault": {
|
"message": "Belirt",
|
||||||
"message": "Öntanımlıyı kullan"
|
"description": "Label for the button to make a style apply only to specific sites"
|
||||||
},
|
},
|
||||||
"confirmDelete": {
|
"installUpdate": {
|
||||||
"message": "Sil"
|
"message": "Güncellemeyi yükle",
|
||||||
},
|
"description": "Label for the button to install an update for a single style"
|
||||||
"confirmDiscardChanges": {
|
},
|
||||||
"message": "Değişiklikleri iptal et?"
|
"sectionRemove": {
|
||||||
},
|
"message": "Bölümü kaldır",
|
||||||
"confirmNo": {
|
"description": "Label for the button to remove a section"
|
||||||
"message": "Hayır"
|
},
|
||||||
},
|
"updateCompleted": {
|
||||||
"confirmOK": {
|
"message": "Güncelleme tamamlandı.",
|
||||||
"message": "Tamam"
|
"description": "Text that displays when an update completed"
|
||||||
},
|
},
|
||||||
"confirmSave": {
|
"checkingForUpdate": {
|
||||||
"message": "Kaydet"
|
"message": "Kontrol ediliyor...",
|
||||||
},
|
"description": "Text to display when checking a style for an update"
|
||||||
"confirmStop": {
|
},
|
||||||
"message": "Dur"
|
"sectionCode": {
|
||||||
},
|
"message": "Kod",
|
||||||
"confirmYes": {
|
"description": "Label for the code for a section"
|
||||||
"message": "Evet"
|
},
|
||||||
},
|
"appliesHelp": {
|
||||||
"connectingDropbox": {
|
"message": "Bu bölümdeki kodun hangi URLlere uygulanacağını sınırlamak için 'Şuraya uygulanır' denetimlerini kullanın.",
|
||||||
"message": "Dropbox'a bağlanıyor..."
|
"description": "Help text for 'applies to' section"
|
||||||
},
|
},
|
||||||
"connectingDropboxNotAllowed": {
|
"editStyleHeading": {
|
||||||
"message": "Dropbox'a bağlanmak yalnızca doğrudan web mağazasından yüklenen uygulamalarda kullanılabilir"
|
"message": "Stili Düzenle",
|
||||||
},
|
"description": "Title of the page for editing styles"
|
||||||
"copied": {
|
},
|
||||||
"message": "Kopyalandı"
|
"addStyleTitle": {
|
||||||
},
|
"message": "Stil Ekleyin",
|
||||||
"copy": {
|
"description": "Title of the page for adding styles"
|
||||||
"message": "Kopyala"
|
},
|
||||||
},
|
"checkAllUpdates": {
|
||||||
"dateInstalled": {
|
"message": "Tüm stiller için güncellemeleri denetle",
|
||||||
"message": "Kurulum tarihi"
|
"description": "Label for the button to check all styles for updates"
|
||||||
},
|
},
|
||||||
"dateUpdated": {
|
"deleteStyleLabel": {
|
||||||
"message": "Güncelleme tarihi"
|
"message": "Sil",
|
||||||
},
|
"description": "Label for the button to delete a style"
|
||||||
"dbError": {
|
},
|
||||||
"message": "Stylus veritabanı kullanılırken bir hata oluştu. Olası çözümler içeren bir web sayfasını ziyaret etmek ister misiniz?"
|
"addStyleLabel": {
|
||||||
},
|
"message": "Yeni stil oluşturun",
|
||||||
"defaultTheme": {
|
"description": "Label for the button to go to the add style page"
|
||||||
"message": "öntanımlı"
|
},
|
||||||
},
|
"editStyleLabel": {
|
||||||
"deleteStyleConfirm": {
|
"message": "Düzenle",
|
||||||
"message": "Bu stili silmek istediğinizden emin misiniz?"
|
"description": "Label for the button to go to the edit style page"
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"description": {
|
||||||
"message": "Sil"
|
"message": "Kullanıcı stil yöneticisi Stylus ile Web'in stilini yenileyin. Stylus Google, Facebook, YouTube, Orkut ve diğer pek çok site için temalar ve görünüm yüklemenize imkân tanır.",
|
||||||
},
|
"description": "Extension description"
|
||||||
"description": {
|
}
|
||||||
"message": "Kullanıcı stil yöneticisi Stylus ile Web'in stilini yenileyin. Stylus Google, Facebook, YouTube, Orkut ve diğer pek çok site için temalar ve görünüm yüklemenize imkân tanır."
|
}
|
||||||
},
|
|
||||||
"disableAllStyles": {
|
|
||||||
"message": "Tüm stilleri kapat"
|
|
||||||
},
|
|
||||||
"disableStyleLabel": {
|
|
||||||
"message": "Devre dışı bırak"
|
|
||||||
},
|
|
||||||
"dragDropMessage": {
|
|
||||||
"message": "İçe aktarmak için yedek dosyanızı bu sayfada herhangi bir yere bırakın."
|
|
||||||
},
|
|
||||||
"dragDropUsercssTabstrip": {
|
|
||||||
"message": "Dosyayı yüklemek için sekme şeridine (sekme başlıklarının gösterildiği alan) bırakın."
|
|
||||||
},
|
|
||||||
"editDeleteText": {
|
|
||||||
"message": "Sil"
|
|
||||||
},
|
|
||||||
"editGotoLine": {
|
|
||||||
"message": "Satıra git (veya satır:sütun)"
|
|
||||||
},
|
|
||||||
"editStyleHeading": {
|
|
||||||
"message": "Stili Düzenle"
|
|
||||||
},
|
|
||||||
"editStyleLabel": {
|
|
||||||
"message": "Düzenle"
|
|
||||||
},
|
|
||||||
"editStyleTitle": {
|
|
||||||
"message": "$stylename$ Stilini Düzenleyin",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enableStyleLabel": {
|
|
||||||
"message": "Etkinleştir"
|
|
||||||
},
|
|
||||||
"excludeStyleByDomainLabel": {
|
|
||||||
"message": "Mevcut alan adını hariç tut"
|
|
||||||
},
|
|
||||||
"excludeStyleByUrlLabel": {
|
|
||||||
"message": "Mevcut URL'yi hariç tut"
|
|
||||||
},
|
|
||||||
"exportLabel": {
|
|
||||||
"message": "Dışa aktar"
|
|
||||||
},
|
|
||||||
"exportSavedSuccess": {
|
|
||||||
"message": "Dosya başarıyla kaydedildi"
|
|
||||||
},
|
|
||||||
"externalFeedback": {
|
|
||||||
"message": "Geribildirim"
|
|
||||||
},
|
|
||||||
"externalHomepage": {
|
|
||||||
"message": "Anasayfa"
|
|
||||||
},
|
|
||||||
"externalLink": {
|
|
||||||
"message": "Harici bağlantı"
|
|
||||||
},
|
|
||||||
"externalSupport": {
|
|
||||||
"message": "Destek"
|
|
||||||
},
|
|
||||||
"externalUsercssDocument": {
|
|
||||||
"message": "Usercss belgeleri"
|
|
||||||
},
|
|
||||||
"filteredStyles": {
|
|
||||||
"message": "Toplam $numTotal$ gösterilen $numShown$",
|
|
||||||
"placeholders": {
|
|
||||||
"numShown": {
|
|
||||||
"content": "$1"
|
|
||||||
},
|
|
||||||
"numTotal": {
|
|
||||||
"content": "$2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"filteredStylesAllHidden": {
|
|
||||||
"message": "Şu anda uygulanan filtreler hiçbir stille eşleşmiyor"
|
|
||||||
},
|
|
||||||
"findStyles": {
|
|
||||||
"message": "Stil bul"
|
|
||||||
},
|
|
||||||
"genericAdd": {
|
|
||||||
"message": "Ekle"
|
|
||||||
},
|
|
||||||
"genericClone": {
|
|
||||||
"message": "Klon"
|
|
||||||
},
|
|
||||||
"genericDisabledLabel": {
|
|
||||||
"message": "Devre dışı"
|
|
||||||
},
|
|
||||||
"genericEnabledLabel": {
|
|
||||||
"message": "Etkin"
|
|
||||||
},
|
|
||||||
"genericError": {
|
|
||||||
"message": "Hata"
|
|
||||||
},
|
|
||||||
"genericHistoryLabel": {
|
|
||||||
"message": "Geçmiş"
|
|
||||||
},
|
|
||||||
"genericNext": {
|
|
||||||
"message": "Sonraki"
|
|
||||||
},
|
|
||||||
"genericPrevious": {
|
|
||||||
"message": "Önceki"
|
|
||||||
},
|
|
||||||
"genericResetLabel": {
|
|
||||||
"message": "Yenile"
|
|
||||||
},
|
|
||||||
"genericSavedMessage": {
|
|
||||||
"message": "Kaydedildi"
|
|
||||||
},
|
|
||||||
"genericTitle": {
|
|
||||||
"message": "Başlık"
|
|
||||||
},
|
|
||||||
"genericUnknown": {
|
|
||||||
"message": "Bilinmiyor"
|
|
||||||
},
|
|
||||||
"gettingStyles": {
|
|
||||||
"message": "Tüm stiller alınıyor..."
|
|
||||||
},
|
|
||||||
"helpAlt": {
|
|
||||||
"message": "Yardım"
|
|
||||||
},
|
|
||||||
"helpKeyMapCommand": {
|
|
||||||
"message": "Bir komut adı yazın"
|
|
||||||
},
|
|
||||||
"helpKeyMapHotkey": {
|
|
||||||
"message": "Bir kısayol tuşuna basın"
|
|
||||||
},
|
|
||||||
"hostDisabled": {
|
|
||||||
"message": "Bu ana bilgisayar, kullanılan tarayıcının mevcut sürümündeki bir hata nedeniyle devre dışı bırakıldı"
|
|
||||||
},
|
|
||||||
"importAppendLabel": {
|
|
||||||
"message": "Stile ekle"
|
|
||||||
},
|
|
||||||
"importAppendTooltip": {
|
|
||||||
"message": "İçe aktarılan stili geçerli stile ekleme"
|
|
||||||
},
|
|
||||||
"importLabel": {
|
|
||||||
"message": "İçe aktar"
|
|
||||||
},
|
|
||||||
"importReplaceLabel": {
|
|
||||||
"message": "Stilin üzerine yaz"
|
|
||||||
},
|
|
||||||
"importReplaceTooltip": {
|
|
||||||
"message": "Geçerli stilin içeriğini atın ve içe aktarılan stil ile üzerine yazın"
|
|
||||||
},
|
|
||||||
"importReportLegendAdded": {
|
|
||||||
"message": "eklendi"
|
|
||||||
},
|
|
||||||
"importReportLegendIdentical": {
|
|
||||||
"message": "Özdeş olan atlandı"
|
|
||||||
},
|
|
||||||
"importReportLegendInvalid": {
|
|
||||||
"message": "geçersiz olan atlandı"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedBoth": {
|
|
||||||
"message": "hem meta bilgi hem de kod güncellendi"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedCode": {
|
|
||||||
"message": "güncellenmiş kod"
|
|
||||||
},
|
|
||||||
"importReportTitle": {
|
|
||||||
"message": "Stillerin içeri aktarılması tamamlandı."
|
|
||||||
},
|
|
||||||
"importReportUnchanged": {
|
|
||||||
"message": "Hiçbir şey değiştirilmedi."
|
|
||||||
},
|
|
||||||
"importReportUndone": {
|
|
||||||
"message": "stiller geri çevrildi"
|
|
||||||
},
|
|
||||||
"importReportUndoneTitle": {
|
|
||||||
"message": "İçe aktarma geri alındı"
|
|
||||||
},
|
|
||||||
"installButton": {
|
|
||||||
"message": "Stil kur"
|
|
||||||
},
|
|
||||||
"installButtonInstalled": {
|
|
||||||
"message": "Stil kuruldu"
|
|
||||||
},
|
|
||||||
"installButtonReinstall": {
|
|
||||||
"message": "Stili yeniden kur"
|
|
||||||
},
|
|
||||||
"installButtonUpdate": {
|
|
||||||
"message": "Stili güncelle"
|
|
||||||
},
|
|
||||||
"installUpdate": {
|
|
||||||
"message": "Güncellemeyi yükle"
|
|
||||||
},
|
|
||||||
"installUpdateFromLabel": {
|
|
||||||
"message": "Güncellemeleri denetle"
|
|
||||||
},
|
|
||||||
"license": {
|
|
||||||
"message": "Lisans"
|
|
||||||
},
|
|
||||||
"linkGetHelp": {
|
|
||||||
"message": "Destek al"
|
|
||||||
},
|
|
||||||
"linkStylusWiki": {
|
|
||||||
"message": "Viki"
|
|
||||||
},
|
|
||||||
"linkTranslate": {
|
|
||||||
"message": "Çevir"
|
|
||||||
},
|
|
||||||
"linterIssues": {
|
|
||||||
"message": "Sorunlar"
|
|
||||||
},
|
|
||||||
"manageFaviconsGray": {
|
|
||||||
"message": "Gri renkte"
|
|
||||||
},
|
|
||||||
"manageFaviconsHelp": {
|
|
||||||
"message": "Stylus harici bir servis kullanır https://icons.duckduckgo.com"
|
|
||||||
},
|
|
||||||
"manageFilters": {
|
|
||||||
"message": "Filtreler"
|
|
||||||
},
|
|
||||||
"manageHeading": {
|
|
||||||
"message": "Yüklü Stiller"
|
|
||||||
},
|
|
||||||
"manageMaxTargets": {
|
|
||||||
"message": "Uygulanan ögelerin sayısı"
|
|
||||||
},
|
|
||||||
"manageNewUI": {
|
|
||||||
"message": "Yeni yönetim ara yüzü düzeni"
|
|
||||||
},
|
|
||||||
"manageOnlyEnabled": {
|
|
||||||
"message": "Yalnızca etkin stiller"
|
|
||||||
},
|
|
||||||
"manageOnlyLocal": {
|
|
||||||
"message": "Yalnızca yerelde oluşturulmuş stiller"
|
|
||||||
},
|
|
||||||
"manageOnlyLocalTooltip": {
|
|
||||||
"message": "Stiller, bir userstyles.org sayfası üzerinden yülenmemiş."
|
|
||||||
},
|
|
||||||
"manageOnlyUpdates": {
|
|
||||||
"message": "Sadece güncellemeler ya da sorunlar ile"
|
|
||||||
},
|
|
||||||
"menuShowBadge": {
|
|
||||||
"message": "Etkin stil sayısını göster"
|
|
||||||
},
|
|
||||||
"noFileToImport": {
|
|
||||||
"message": "Stillerinizi içe aktarmak için önce dışa aktarmanız gerekir."
|
|
||||||
},
|
|
||||||
"noStylesForSite": {
|
|
||||||
"message": "Bu site için hiçbir stil yüklenmedi."
|
|
||||||
},
|
|
||||||
"openManage": {
|
|
||||||
"message": "Yüklü stilleri yönet"
|
|
||||||
},
|
|
||||||
"openOptions": {
|
|
||||||
"message": "Seçenekler"
|
|
||||||
},
|
|
||||||
"openStylesManager": {
|
|
||||||
"message": "Stil yöneticisini aç"
|
|
||||||
},
|
|
||||||
"optionsActions": {
|
|
||||||
"message": "İşlemler"
|
|
||||||
},
|
|
||||||
"optionsAdvanced": {
|
|
||||||
"message": "Gelişmiş"
|
|
||||||
},
|
|
||||||
"optionsAdvancedContextDelete": {
|
|
||||||
"message": "Editör bağlam menüsüne 'Sil' seçeneği ekle"
|
|
||||||
},
|
|
||||||
"optionsAdvancedExposeIframes": {
|
|
||||||
"message": "HTML[stylus-iframe] vasıtasıyla iframe'leri gösterin"
|
|
||||||
},
|
|
||||||
"optionsBadgeDisabled": {
|
|
||||||
"message": "Etkin değilken arkaplan rengi"
|
|
||||||
},
|
|
||||||
"optionsBadgeNormal": {
|
|
||||||
"message": "Arkaplan rengi"
|
|
||||||
},
|
|
||||||
"optionsCheck": {
|
|
||||||
"message": "Stilleri güncelle"
|
|
||||||
},
|
|
||||||
"optionsCheckUpdate": {
|
|
||||||
"message": "Uygun tüm güncellemeleri kontrol et ve yükle."
|
|
||||||
},
|
|
||||||
"optionsCustomizePopup": {
|
|
||||||
"message": "Açılır pencere"
|
|
||||||
},
|
|
||||||
"optionsCustomizeUpdate": {
|
|
||||||
"message": "Güncellemeler"
|
|
||||||
},
|
|
||||||
"optionsHeading": {
|
|
||||||
"message": "Seçenekler"
|
|
||||||
},
|
|
||||||
"optionsOpen": {
|
|
||||||
"message": "Aç"
|
|
||||||
},
|
|
||||||
"optionsOpenManager": {
|
|
||||||
"message": "Stilleri yönet"
|
|
||||||
},
|
|
||||||
"optionsPopupWidth": {
|
|
||||||
"message": "Popup genişliği (piksel cinsinden)"
|
|
||||||
},
|
|
||||||
"optionsReset": {
|
|
||||||
"message": "Ayarları varsayılanlara sıfırla."
|
|
||||||
},
|
|
||||||
"optionsResetButton": {
|
|
||||||
"message": "Sıfırlama seçenekleri"
|
|
||||||
},
|
|
||||||
"optionsSubheading": {
|
|
||||||
"message": "Daha Fazla Seçenek"
|
|
||||||
},
|
|
||||||
"optionsSyncConnect": {
|
|
||||||
"message": "Bağlan"
|
|
||||||
},
|
|
||||||
"optionsSyncDisconnect": {
|
|
||||||
"message": "Bağlantıyı kes"
|
|
||||||
},
|
|
||||||
"optionsSyncLogin": {
|
|
||||||
"message": "Giriş yap"
|
|
||||||
},
|
|
||||||
"optionsSyncStatusConnected": {
|
|
||||||
"message": "Bağlandı"
|
|
||||||
},
|
|
||||||
"optionsSyncStatusConnecting": {
|
|
||||||
"message": "Bağlanılıyor..."
|
|
||||||
},
|
|
||||||
"optionsSyncStatusDisconnected": {
|
|
||||||
"message": "Bağlantı kesildi"
|
|
||||||
},
|
|
||||||
"optionsSyncStatusDisconnecting": {
|
|
||||||
"message": "Bağlantı kesiliyor..."
|
|
||||||
},
|
|
||||||
"optionsUpdateImportNote": {
|
|
||||||
"message": "Eski versiyonlardan ya da Stylish'ten stil yedeklerini içeri aktarırken, hepsinin güncel olduğundan emin olmak için stil yöneticisinden bir defa elle güncelleme kontrolü yapın"
|
|
||||||
},
|
|
||||||
"optionsUpdateInterval": {
|
|
||||||
"message": "Saat cinsinden kullanıcı stili otomatik güncelleme aralığı (devre dışı bırakmak için 0 yazın) "
|
|
||||||
},
|
|
||||||
"overwriteFileExport": {
|
|
||||||
"message": "Mevcut bir dosyanın üzerine yazmak istiyor musunuz?"
|
|
||||||
},
|
|
||||||
"paginationCurrent": {
|
|
||||||
"message": "Şimdiki sayfa"
|
|
||||||
},
|
|
||||||
"paginationEstimated": {
|
|
||||||
"message": "Tahmini sayfa sayısı"
|
|
||||||
},
|
|
||||||
"paginationNext": {
|
|
||||||
"message": "Sonraki sayfa"
|
|
||||||
},
|
|
||||||
"paginationPrevious": {
|
|
||||||
"message": "Önceki sayfa"
|
|
||||||
},
|
|
||||||
"paginationTotal": {
|
|
||||||
"message": "Toplam sayfa"
|
|
||||||
},
|
|
||||||
"prefShowBadge": {
|
|
||||||
"message": "Şu an açık olan site için aktif olan stil sayısı"
|
|
||||||
},
|
|
||||||
"previewLabel": {
|
|
||||||
"message": "Canlı önizleme"
|
|
||||||
},
|
|
||||||
"readingStyles": {
|
|
||||||
"message": "Stiller okunuyor..."
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"message": "Değiştir"
|
|
||||||
},
|
|
||||||
"replaceAll": {
|
|
||||||
"message": "Hepsini değiştir"
|
|
||||||
},
|
|
||||||
"replaceWith": {
|
|
||||||
"message": "Şununla değiştir"
|
|
||||||
},
|
|
||||||
"retrieveBckp": {
|
|
||||||
"message": "Stilleri içe aktar"
|
|
||||||
},
|
|
||||||
"retrieveDropboxSync": {
|
|
||||||
"message": "Dropbox'a aktar"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"message": "Ara"
|
|
||||||
},
|
|
||||||
"searchRegexp": {
|
|
||||||
"message": "Regexp araması için /re/ sözdizimini (syntax) kullanın."
|
|
||||||
},
|
|
||||||
"searchResultInstallCount": {
|
|
||||||
"message": "Toplam kurulum sayısı"
|
|
||||||
},
|
|
||||||
"searchResultNoneFound": {
|
|
||||||
"message": "Bu site için stil bulunamadı."
|
|
||||||
},
|
|
||||||
"searchResultWeeklyCount": {
|
|
||||||
"message": "Haftalık kurulum sayısı"
|
|
||||||
},
|
|
||||||
"sectionAdd": {
|
|
||||||
"message": "Başka bölüm ekle"
|
|
||||||
},
|
|
||||||
"sectionCode": {
|
|
||||||
"message": "Kod"
|
|
||||||
},
|
|
||||||
"sectionRemove": {
|
|
||||||
"message": "Bölümü kaldır"
|
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"message": "Bölümler"
|
|
||||||
},
|
|
||||||
"shortcuts": {
|
|
||||||
"message": "Kısayollar"
|
|
||||||
},
|
|
||||||
"shortcutsNote": {
|
|
||||||
"message": "Klavye kısayolları tanımla"
|
|
||||||
},
|
|
||||||
"sortDateNewestFirst": {
|
|
||||||
"message": "önce en yeniler"
|
|
||||||
},
|
|
||||||
"sortDateOldestFirst": {
|
|
||||||
"message": "önce en eskiler"
|
|
||||||
},
|
|
||||||
"sortStylesHelpTitle": {
|
|
||||||
"message": "İçeriği sırala"
|
|
||||||
},
|
|
||||||
"styleBadRegexp": {
|
|
||||||
"message": "Regexp geçerli değil."
|
|
||||||
},
|
|
||||||
"styleBeautify": {
|
|
||||||
"message": "Güzelleştir"
|
|
||||||
},
|
|
||||||
"styleCancelEditLabel": {
|
|
||||||
"message": "Yönetim sayfasına dön"
|
|
||||||
},
|
|
||||||
"styleChangesNotSaved": {
|
|
||||||
"message": "Bu stilde yaptığınız, kaydedilmemiş değişiklikler var."
|
|
||||||
},
|
|
||||||
"styleEnabledLabel": {
|
|
||||||
"message": "Etkin"
|
|
||||||
},
|
|
||||||
"styleFromMozillaFormatPrompt": {
|
|
||||||
"message": "Mozilla biçemiyle yazılmış kodu yapıştır"
|
|
||||||
},
|
|
||||||
"styleInstall": {
|
|
||||||
"message": "'$stylename$' Stylus'e yüklensin mi?",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleMissingName": {
|
|
||||||
"message": "Bir ad girin"
|
|
||||||
},
|
|
||||||
"styleMozillaFormatHeading": {
|
|
||||||
"message": "Mozilla Biçemi"
|
|
||||||
},
|
|
||||||
"styleRegexpInvalidExplanation": {
|
|
||||||
"message": "Hiç derlenemeyen bazı 'regexp ()' kuralları."
|
|
||||||
},
|
|
||||||
"styleRegexpProblemTooltip": {
|
|
||||||
"message": "'Regexp ()' yanlış kullanımı nedeniyle uygulanmayan bölüm sayısı"
|
|
||||||
},
|
|
||||||
"styleRegexpTestFull": {
|
|
||||||
"message": "Eşleşen sekmeler"
|
|
||||||
},
|
|
||||||
"styleRegexpTestInvalid": {
|
|
||||||
"message": "Geçersiz regexp'ler atlandı"
|
|
||||||
},
|
|
||||||
"styleRegexpTestNone": {
|
|
||||||
"message": "Eşleşen sekme yok"
|
|
||||||
},
|
|
||||||
"styleRegexpTestPartial": {
|
|
||||||
"message": "Tam olarak eşleşmediğinden atlandı"
|
|
||||||
},
|
|
||||||
"styleRegexpTestTitle": {
|
|
||||||
"message": "Eşleşen açık sekmelerin listesi (sekmesine gitmek için URL'ye tıklayın)"
|
|
||||||
},
|
|
||||||
"styleSaveLabel": {
|
|
||||||
"message": "Kaydet"
|
|
||||||
},
|
|
||||||
"styleToMozillaFormatHelp": {
|
|
||||||
"message": "Kodun Mozilla biçimi, Firefox için Stylish ile kullanılabilir ve userstyles.org sitesine gönderilebilir."
|
|
||||||
},
|
|
||||||
"styleToMozillaFormatTitle": {
|
|
||||||
"message": "Mozilla biçeminde bir stil"
|
|
||||||
},
|
|
||||||
"styleUpdate": {
|
|
||||||
"message": "\";$stylename$\" stilini güncelleştirmek istiyor musunuz?",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stylusUnavailableForURL": {
|
|
||||||
"message": "Stylus bunun gibi sayfalarda çalışmaz."
|
|
||||||
},
|
|
||||||
"stylusUnavailableForURLdetails": {
|
|
||||||
"message": "Bir güvenlik önlemi olarak tarayıcı eklentilerin, kendinin yerleşik sayfalarını (chrome://version, Chrome 61'den itibaren standart yeni sekme sayfası, about: addons vb.) ve diğer uzantı sayfalarını etkilemesini engeller. Ayrıca her tarayıcı kendi uzantı galerisine (Chrome Web Mağazası veya AMO gibi) erişimi de kısıtlar. "
|
|
||||||
},
|
|
||||||
"syncDropboxDeprecated": {
|
|
||||||
"message": "Dropbox içe/dışa aktarma, seçenekler sayfasında daha gelişmiş bir stil senkronizasyonu ile değiştirilir."
|
|
||||||
},
|
|
||||||
"syncDropboxStyles": {
|
|
||||||
"message": "Dropbox'tan aktar"
|
|
||||||
},
|
|
||||||
"toggleStyle": {
|
|
||||||
"message": "Stili değiştir"
|
|
||||||
},
|
|
||||||
"undo": {
|
|
||||||
"message": "Geri al"
|
|
||||||
},
|
|
||||||
"undoGlobal": {
|
|
||||||
"message": "Tüm bölümlerde geri alma işlemi uygula."
|
|
||||||
},
|
|
||||||
"unreachableContentScript": {
|
|
||||||
"message": "Sayfayla iletişim kurulamadı. Sekmeyi yeniden yüklemeyi deneyin."
|
|
||||||
},
|
|
||||||
"unreachableFileHint": {
|
|
||||||
"message": "Stylus, eğer siz Stylus chrome://extensions sayfasından ilgili onay kutusunu işaretlerseniz file:// adreslerine erişebilir."
|
|
||||||
},
|
|
||||||
"unzipStyles": {
|
|
||||||
"message": "Stiller açılıyor..."
|
|
||||||
},
|
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
|
||||||
"message": "Güncelleme bulunamadı."
|
|
||||||
},
|
|
||||||
"updateCheckFailBadResponseCode": {
|
|
||||||
"message": "Güncellenemedi: sunucu yanıt olarak $code$ kodunu gönderdi.",
|
|
||||||
"placeholders": {
|
|
||||||
"code": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"updateCheckFailServerUnreachable": {
|
|
||||||
"message": "Güncellenemedi: sunucuya erişilemiyor."
|
|
||||||
},
|
|
||||||
"updateCheckHistory": {
|
|
||||||
"message": "Güncelleme kontrolü geçmişi"
|
|
||||||
},
|
|
||||||
"updateCheckManualUpdateForce": {
|
|
||||||
"message": "Güncellemeyi kur (yerel düzenlemelerin üzerine yazılacak)"
|
|
||||||
},
|
|
||||||
"updateCheckManualUpdateHint": {
|
|
||||||
"message": "Bir güncellemeyi zorla yapmak yerel düzenlemelerin üstüne yazacaktır."
|
|
||||||
},
|
|
||||||
"updateCheckSkippedLocallyEdited": {
|
|
||||||
"message": "Bu stil yerel olarak düzenlendi."
|
|
||||||
},
|
|
||||||
"updateCheckSkippedMaybeLocallyEdited": {
|
|
||||||
"message": "Bu stil yerel olarak düzenlenmiş olabilir."
|
|
||||||
},
|
|
||||||
"updateCheckSucceededNoUpdate": {
|
|
||||||
"message": "Stil güncel."
|
|
||||||
},
|
|
||||||
"updateCompleted": {
|
|
||||||
"message": "Güncelleme tamamlandı."
|
|
||||||
},
|
|
||||||
"updatesCurrentlyInstalled": {
|
|
||||||
"message": "Kurulan güncellemeler:"
|
|
||||||
},
|
|
||||||
"uploadingFile": {
|
|
||||||
"message": "Dosya Yükleniyor..."
|
|
||||||
},
|
|
||||||
"usercssAvoidOverwriting": {
|
|
||||||
"message": "Mevcut bir stilin üzerine yazmaktan kaçınmak için lütfen @name veya @ ad alanının değerini değiştirin."
|
|
||||||
},
|
|
||||||
"usercssConfigIncomplete": {
|
|
||||||
"message": "Stil, yapılandırma iletişim kutusu gösterildikten sonra güncellendi veya silindi. Bu değişkenler, stilin meta verilerini bozmamak için kaydedilmedi:"
|
|
||||||
},
|
|
||||||
"usercssEditorNamePlaceholder": {
|
|
||||||
"message": "Kodda @name belirtin"
|
|
||||||
},
|
|
||||||
"usercssReplaceTemplateConfirmation": {
|
|
||||||
"message": "Yeni Usercss stilleri için varsayılan şablonu geçerli kodla değiştir?"
|
|
||||||
},
|
|
||||||
"usercssReplaceTemplateSectionBody": {
|
|
||||||
"message": "Kodu buraya ekle..."
|
|
||||||
},
|
|
||||||
"versionInvalidOlder": {
|
|
||||||
"message": "Sürüm, kurulu stillerden daha eski."
|
|
||||||
},
|
|
||||||
"writeStyleFor": {
|
|
||||||
"message": "Şunun için stil yaz:"
|
|
||||||
},
|
|
||||||
"writeStyleForURL": {
|
|
||||||
"message": "bu URL"
|
|
||||||
},
|
|
||||||
"zipStyles": {
|
|
||||||
"message": "Stiller sıkıştırılıyor..."
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,285 +0,0 @@
|
||||||
{
|
|
||||||
"InaccessibleFileHint": {
|
|
||||||
"message": "Stylus не може отримати доступ до деяких типів файлів (наприклад, файли PDF і JSON)."
|
|
||||||
},
|
|
||||||
"addStyleLabel": {
|
|
||||||
"message": "Написати новий стиль"
|
|
||||||
},
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Додати стиль"
|
|
||||||
},
|
|
||||||
"alphaChannel": {
|
|
||||||
"message": "Прозорість"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Додати"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Застосувати до $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "та інші"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "URL в домені"
|
|
||||||
},
|
|
||||||
"appliesHelp": {
|
|
||||||
"message": "Щоб вказати, до яких URL відноситься код в цьому розділі, скористайтеся параметром \"Застосувати до\"."
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Застосувати до"
|
|
||||||
},
|
|
||||||
"appliesLineWidgetLabel": {
|
|
||||||
"message": "Показати цільові сайти секцій"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Видалити"
|
|
||||||
},
|
|
||||||
"appliesRemoveError": {
|
|
||||||
"message": "Не можна видалити останній елемент"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Вказати"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Все"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URL, що починаються з "
|
|
||||||
},
|
|
||||||
"applyAllUpdates": {
|
|
||||||
"message": "Застосувати всі оновлення"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"message": "Автор"
|
|
||||||
},
|
|
||||||
"backupButtons": {
|
|
||||||
"message": "Резервне копіювання"
|
|
||||||
},
|
|
||||||
"bckpInstStyles": {
|
|
||||||
"message": "Експорт стилів"
|
|
||||||
},
|
|
||||||
"checkingForUpdate": {
|
|
||||||
"message": "Перевірка ... "
|
|
||||||
},
|
|
||||||
"clickToUninstall": {
|
|
||||||
"message": "Натисніть, щоб видалити "
|
|
||||||
},
|
|
||||||
"cm_selectByTokensTooltip": {
|
|
||||||
"message": "Приклади токенов: .foo-бар-2 #aabbcc 0,32 !important\nЯкщо вимкнено: вибираються слова, розділені розділовими знаками."
|
|
||||||
},
|
|
||||||
"confirmCancel": {
|
|
||||||
"message": "Скасувати "
|
|
||||||
},
|
|
||||||
"confirmDelete": {
|
|
||||||
"message": "Видалити "
|
|
||||||
},
|
|
||||||
"confirmNo": {
|
|
||||||
"message": "Ні"
|
|
||||||
},
|
|
||||||
"confirmSave": {
|
|
||||||
"message": "Зберегти"
|
|
||||||
},
|
|
||||||
"confirmStop": {
|
|
||||||
"message": "Зупинити"
|
|
||||||
},
|
|
||||||
"confirmYes": {
|
|
||||||
"message": "Так"
|
|
||||||
},
|
|
||||||
"deleteStyleLabel": {
|
|
||||||
"message": "Видалити"
|
|
||||||
},
|
|
||||||
"disableStyleLabel": {
|
|
||||||
"message": "Вимкнути"
|
|
||||||
},
|
|
||||||
"editDeleteText": {
|
|
||||||
"message": "Видалити"
|
|
||||||
},
|
|
||||||
"findStyles": {
|
|
||||||
"message": "Знайти стилі"
|
|
||||||
},
|
|
||||||
"genericAdd": {
|
|
||||||
"message": "Додати"
|
|
||||||
},
|
|
||||||
"genericDescription": {
|
|
||||||
"message": "Опис"
|
|
||||||
},
|
|
||||||
"genericDisabledLabel": {
|
|
||||||
"message": "Відключено"
|
|
||||||
},
|
|
||||||
"genericEnabledLabel": {
|
|
||||||
"message": "Увімкнено"
|
|
||||||
},
|
|
||||||
"genericError": {
|
|
||||||
"message": "Помилка"
|
|
||||||
},
|
|
||||||
"genericHistoryLabel": {
|
|
||||||
"message": "Історія"
|
|
||||||
},
|
|
||||||
"genericNext": {
|
|
||||||
"message": "Наступний "
|
|
||||||
},
|
|
||||||
"genericPrevious": {
|
|
||||||
"message": "Попередній"
|
|
||||||
},
|
|
||||||
"genericResetLabel": {
|
|
||||||
"message": "Скинути"
|
|
||||||
},
|
|
||||||
"genericSavedMessage": {
|
|
||||||
"message": "Збережено"
|
|
||||||
},
|
|
||||||
"genericTitle": {
|
|
||||||
"message": "Ім'я"
|
|
||||||
},
|
|
||||||
"genericUnknown": {
|
|
||||||
"message": "Невідомо"
|
|
||||||
},
|
|
||||||
"gettingStyles": {
|
|
||||||
"message": "Отримання всіх стилів ..."
|
|
||||||
},
|
|
||||||
"helpAlt": {
|
|
||||||
"message": "Довідка"
|
|
||||||
},
|
|
||||||
"helpKeyMapCommand": {
|
|
||||||
"message": "Введіть ім'я команди"
|
|
||||||
},
|
|
||||||
"helpKeyMapHotkey": {
|
|
||||||
"message": "Натисніть клавішу"
|
|
||||||
},
|
|
||||||
"hostDisabled": {
|
|
||||||
"message": "Цей хост вимкнено через помилку в поточній версії браузера, що використовується"
|
|
||||||
},
|
|
||||||
"importLabel": {
|
|
||||||
"message": "Імпорт"
|
|
||||||
},
|
|
||||||
"importReplaceLabel": {
|
|
||||||
"message": "Замінити стиль"
|
|
||||||
},
|
|
||||||
"importReportLegendIdentical": {
|
|
||||||
"message": "ідентичні пропущені"
|
|
||||||
},
|
|
||||||
"importReportLegendInvalid": {
|
|
||||||
"message": "недісних пропущено"
|
|
||||||
},
|
|
||||||
"importReportTitle": {
|
|
||||||
"message": "Імпорт стилів закінчено"
|
|
||||||
},
|
|
||||||
"installUpdate": {
|
|
||||||
"message": "Встановити оновлення"
|
|
||||||
},
|
|
||||||
"manageFavicons": {
|
|
||||||
"message": "Піктограми для цільових сайтів"
|
|
||||||
},
|
|
||||||
"manageFilters": {
|
|
||||||
"message": "Фільтри"
|
|
||||||
},
|
|
||||||
"manageHeading": {
|
|
||||||
"message": "Встановити Styles"
|
|
||||||
},
|
|
||||||
"manageNewUI": {
|
|
||||||
"message": "Новий макет інтерфейсу управління користувача"
|
|
||||||
},
|
|
||||||
"meta_unknownJSONLiteral": {
|
|
||||||
"message": "Невірний JSON: $literal$не є дійсним літералом JSON",
|
|
||||||
"placeholders": {
|
|
||||||
"literal": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"openManage": {
|
|
||||||
"message": "Керування"
|
|
||||||
},
|
|
||||||
"openOptions": {
|
|
||||||
"message": "Налаштування"
|
|
||||||
},
|
|
||||||
"optionsHeading": {
|
|
||||||
"message": "Налаштування"
|
|
||||||
},
|
|
||||||
"optionsReset": {
|
|
||||||
"message": "Скидання налаштувань до значень за замовчуванням"
|
|
||||||
},
|
|
||||||
"optionsResetButton": {
|
|
||||||
"message": "Скинути параметри"
|
|
||||||
},
|
|
||||||
"optionsSubheading": {
|
|
||||||
"message": "Додатково"
|
|
||||||
},
|
|
||||||
"optionsSyncLogin": {
|
|
||||||
"message": "Логін"
|
|
||||||
},
|
|
||||||
"optionsSyncStatusRelogin": {
|
|
||||||
"message": "Сеанс закінчився, будь ласка, увійдіть ще раз."
|
|
||||||
},
|
|
||||||
"paginationNext": {
|
|
||||||
"message": "Наступна сторінка"
|
|
||||||
},
|
|
||||||
"paginationPrevious": {
|
|
||||||
"message": "Попередня сторінка"
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"message": "Замінити"
|
|
||||||
},
|
|
||||||
"replaceAll": {
|
|
||||||
"message": "Замінити все"
|
|
||||||
},
|
|
||||||
"retrieveBckp": {
|
|
||||||
"message": "Імпорт стилів"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"message": "Пошук"
|
|
||||||
},
|
|
||||||
"searchStylesAll": {
|
|
||||||
"message": "Усі"
|
|
||||||
},
|
|
||||||
"searchStylesCode": {
|
|
||||||
"message": "CSS код"
|
|
||||||
},
|
|
||||||
"searchStylesHelp": {
|
|
||||||
"message": "</> або <Ctrl-F>клавіша фокусує поле пошуку.\nРежим за замовчуванням — це пошук у звичайному тексті для всіх термінів, розділених пробілами, у будь-якому порядку.\nТочні слова: оберніть запит у подвійні лапки, напр. <.header ~ div\">\nРегулярні вирази: включають косі риски та прапорці, напр.</body.*?\\ba\\b/i>\n«By URL» в селекторі області: знаходить стилі, які застосовуються до повністю вказаної URL-адреси, напр. https://www.example.org/\n\"Metadata\" в селектрі області: шукає в іменах, \"applies to\" специфікаторів, URL-адреси встановлення, URL-адресі оновлення та всьому блоку метаданих для стилів CSS користувача."
|
|
||||||
},
|
|
||||||
"searchStylesName": {
|
|
||||||
"message": "Назва"
|
|
||||||
},
|
|
||||||
"sectionCode": {
|
|
||||||
"message": "Код"
|
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"message": "Розділи"
|
|
||||||
},
|
|
||||||
"styleBeautify": {
|
|
||||||
"message": "Облагородити"
|
|
||||||
},
|
|
||||||
"styleCancelEditLabel": {
|
|
||||||
"message": "Повернутися до керування"
|
|
||||||
},
|
|
||||||
"styleEnabledLabel": {
|
|
||||||
"message": "Увімкнено"
|
|
||||||
},
|
|
||||||
"stylePreferSchemeLabel": {
|
|
||||||
"message": "Темна/Світла тема"
|
|
||||||
},
|
|
||||||
"styleSaveLabel": {
|
|
||||||
"message": "Зберегти"
|
|
||||||
},
|
|
||||||
"syncErrorRelogin": {
|
|
||||||
"message": "Помилка синхронізації. Ви вийшли із системи. \nСпробуйте повторно увійти до системи в налаштуваннях Stylus."
|
|
||||||
},
|
|
||||||
"toggleStyle": {
|
|
||||||
"message": "Включити/виключити стиль"
|
|
||||||
},
|
|
||||||
"writeStyleFor": {
|
|
||||||
"message": "Створити стиль для:"
|
|
||||||
},
|
|
||||||
"writeStyleForURL": {
|
|
||||||
"message": "цей URL"
|
|
||||||
},
|
|
||||||
"zipStyles": {
|
|
||||||
"message": "Запаковування стилів ... "
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,379 +0,0 @@
|
||||||
{
|
|
||||||
"InaccessibleFileHint": {
|
|
||||||
"message": "Stylus không thể truy cập một số kiểu tập tin (chẳng hạn như PDF và JSON)."
|
|
||||||
},
|
|
||||||
"addStyleLabel": {
|
|
||||||
"message": "Viết bảng định kiểu mới"
|
|
||||||
},
|
|
||||||
"addStyleTitle": {
|
|
||||||
"message": "Thêm bảng định kiểu"
|
|
||||||
},
|
|
||||||
"alphaChannel": {
|
|
||||||
"message": "Độ mờ"
|
|
||||||
},
|
|
||||||
"appliesAdd": {
|
|
||||||
"message": "Thêm"
|
|
||||||
},
|
|
||||||
"appliesDisplay": {
|
|
||||||
"message": "Áp dụng với: $applies$",
|
|
||||||
"placeholders": {
|
|
||||||
"applies": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"appliesDisplayTruncatedSuffix": {
|
|
||||||
"message": "và một số khác"
|
|
||||||
},
|
|
||||||
"appliesDomainOption": {
|
|
||||||
"message": "Các địa chỉ thuộc tên miền này"
|
|
||||||
},
|
|
||||||
"appliesHelp": {
|
|
||||||
"message": "Dùng tuỳ chọn \"Áp dụng với\" để giới hạn các địa chỉ cho đoạn mã này"
|
|
||||||
},
|
|
||||||
"appliesLabel": {
|
|
||||||
"message": "Áp dụng với"
|
|
||||||
},
|
|
||||||
"appliesLineWidgetLabel": {
|
|
||||||
"message": "Hiển thị thông tin \"Áp dụng với\""
|
|
||||||
},
|
|
||||||
"appliesLineWidgetWarning": {
|
|
||||||
"message": "Không hoạt động với CSS tối giản"
|
|
||||||
},
|
|
||||||
"appliesRegexpOption": {
|
|
||||||
"message": "URL khớp với biểu thức chính quy"
|
|
||||||
},
|
|
||||||
"appliesRemove": {
|
|
||||||
"message": "Xoá"
|
|
||||||
},
|
|
||||||
"appliesRemoveError": {
|
|
||||||
"message": "Không thể xoá mục \"Áp dụng với\" cuối cùng"
|
|
||||||
},
|
|
||||||
"appliesSpecify": {
|
|
||||||
"message": "Chỉ định"
|
|
||||||
},
|
|
||||||
"appliesToEverything": {
|
|
||||||
"message": "Tất cả"
|
|
||||||
},
|
|
||||||
"appliesUrlPrefixOption": {
|
|
||||||
"message": "URL bắt đầu bằng"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"message": "Tác giả"
|
|
||||||
},
|
|
||||||
"backupButtons": {
|
|
||||||
"message": "Sao lưu"
|
|
||||||
},
|
|
||||||
"backupMessage": {
|
|
||||||
"message": "Để nhập tập tin sao lưu, kéo và thả nó vào trang này hoặc nhấp vào nút Nhập.\n\nĐể xuất một bản sao lưu tương thích với Stylus trước phiên bản 1.5.18, nhấp chuột phải hoặc nhấn giữ Shift khi nhấp chuột trái vào nút Xuất."
|
|
||||||
},
|
|
||||||
"bckpInstStyles": {
|
|
||||||
"message": "Xuất bảng định kiểu"
|
|
||||||
},
|
|
||||||
"checkForUpdate": {
|
|
||||||
"message": "Kiểm tra bản cập nhật mới"
|
|
||||||
},
|
|
||||||
"checkingForUpdate": {
|
|
||||||
"message": "Đang kiểm tra..."
|
|
||||||
},
|
|
||||||
"clickToUninstall": {
|
|
||||||
"message": "Nhấp để huỷ kích hoạt"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBrackets": {
|
|
||||||
"message": "Tự động đóng ngoặc và nháy"
|
|
||||||
},
|
|
||||||
"cm_autoCloseBracketsTooltip": {
|
|
||||||
"message": "Tự động thêm dấu đóng tương ứng khi nhập một trong các dấu (, [, {, ' và \"."
|
|
||||||
},
|
|
||||||
"cm_autocompleteOnTyping": {
|
|
||||||
"message": "Tự động hoàn thành"
|
|
||||||
},
|
|
||||||
"cm_colorpicker": {
|
|
||||||
"message": "Bộ chọn màu cho màu CSS"
|
|
||||||
},
|
|
||||||
"cm_indentWithTabs": {
|
|
||||||
"message": "Lùi đầu dòng thông minh bằng tab"
|
|
||||||
},
|
|
||||||
"cm_lineWrapping": {
|
|
||||||
"message": "Gập dòng dài"
|
|
||||||
},
|
|
||||||
"cm_linter": {
|
|
||||||
"message": "Trình phân tích cú pháp"
|
|
||||||
},
|
|
||||||
"cm_matchHighlight": {
|
|
||||||
"message": "Làm nổi"
|
|
||||||
},
|
|
||||||
"cm_matchHighlightSelection": {
|
|
||||||
"message": "Chỉ vùng được chọn"
|
|
||||||
},
|
|
||||||
"cm_matchHighlightToken": {
|
|
||||||
"message": "Token nằm dưới con trỏ văn bản"
|
|
||||||
},
|
|
||||||
"cm_selectByTokens": {
|
|
||||||
"message": "Nhấp đúp để chọn token"
|
|
||||||
},
|
|
||||||
"cm_selectByTokensTooltip": {
|
|
||||||
"message": "Ví dụ về token: .foo-bar-2 #aabbcc 0.32 !important\nKhi tắt: Chọn từ (phân tách bằng dấu câu)."
|
|
||||||
},
|
|
||||||
"cm_smartIndent": {
|
|
||||||
"message": "Lùi đầu dòng thông minh"
|
|
||||||
},
|
|
||||||
"cm_tabSize": {
|
|
||||||
"message": "Chiều rộng tab"
|
|
||||||
},
|
|
||||||
"cm_theme": {
|
|
||||||
"message": "Chủ đề"
|
|
||||||
},
|
|
||||||
"colorpickerSwitchFormatTooltip": {
|
|
||||||
"message": "Đổi định dạng: HEX → RGB → HSL.\nNhấn giữ phím Shift khi nhấp để đảo thứ tự.\nPhím tắt: PgUp và PgDn."
|
|
||||||
},
|
|
||||||
"colorpickerTooltip": {
|
|
||||||
"message": "Mở bộ chọn màu"
|
|
||||||
},
|
|
||||||
"configOnChange": {
|
|
||||||
"message": "khi thay đổi"
|
|
||||||
},
|
|
||||||
"configOnChangeTooltip": {
|
|
||||||
"message": "Tự động lưu và áp dụng"
|
|
||||||
},
|
|
||||||
"configureStyle": {
|
|
||||||
"message": "Thiết lập"
|
|
||||||
},
|
|
||||||
"configureStyleOnHomepage": {
|
|
||||||
"message": "Thiết lập trên trang chủ"
|
|
||||||
},
|
|
||||||
"confirmCancel": {
|
|
||||||
"message": "Huỷ"
|
|
||||||
},
|
|
||||||
"confirmClose": {
|
|
||||||
"message": "Đóng"
|
|
||||||
},
|
|
||||||
"confirmDefault": {
|
|
||||||
"message": "Dùng mặc định"
|
|
||||||
},
|
|
||||||
"confirmDelete": {
|
|
||||||
"message": "Xoá"
|
|
||||||
},
|
|
||||||
"confirmDiscardChanges": {
|
|
||||||
"message": "Huỷ thay đổi?"
|
|
||||||
},
|
|
||||||
"confirmNo": {
|
|
||||||
"message": "Không"
|
|
||||||
},
|
|
||||||
"confirmSave": {
|
|
||||||
"message": "Lưu"
|
|
||||||
},
|
|
||||||
"confirmStop": {
|
|
||||||
"message": "Dừng"
|
|
||||||
},
|
|
||||||
"confirmYes": {
|
|
||||||
"message": "Có"
|
|
||||||
},
|
|
||||||
"connectingDropbox": {
|
|
||||||
"message": "Đang kết nối với Dropbox..."
|
|
||||||
},
|
|
||||||
"connectingDropboxNotAllowed": {
|
|
||||||
"message": "Tính năng kết nối với Dropbox chỉ khả dụng khi cài ứng dụng trực tiếp từ cửa hàng web"
|
|
||||||
},
|
|
||||||
"copied": {
|
|
||||||
"message": "Đã chép vào khay nhớ tạm"
|
|
||||||
},
|
|
||||||
"copy": {
|
|
||||||
"message": "Chép vào khay nhớ tạm"
|
|
||||||
},
|
|
||||||
"dateAbbrDay": {
|
|
||||||
"message": "$value$ ngày",
|
|
||||||
"placeholders": {
|
|
||||||
"value": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dateAbbrHour": {
|
|
||||||
"message": "$value$ giờ",
|
|
||||||
"placeholders": {
|
|
||||||
"value": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dateAbbrMonth": {
|
|
||||||
"message": "$value$ tháng",
|
|
||||||
"placeholders": {
|
|
||||||
"value": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dateAbbrYear": {
|
|
||||||
"message": "$value$ năm",
|
|
||||||
"placeholders": {
|
|
||||||
"value": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dateInstalled": {
|
|
||||||
"message": "Ngày cài đặt"
|
|
||||||
},
|
|
||||||
"dateUpdated": {
|
|
||||||
"message": "Ngày cập nhật"
|
|
||||||
},
|
|
||||||
"defaultTheme": {
|
|
||||||
"message": "mặc định"
|
|
||||||
},
|
|
||||||
"deleteStyleConfirm": {
|
|
||||||
"message": "Bạn có chắc chắn muốn xoá bảng định kiểu này không?"
|
|
||||||
},
|
|
||||||
"deleteStyleLabel": {
|
|
||||||
"message": "Xoá"
|
|
||||||
},
|
|
||||||
"disableAllStyles": {
|
|
||||||
"message": "Tắt tất cả bảng định kiểu"
|
|
||||||
},
|
|
||||||
"disableAllStylesOff": {
|
|
||||||
"message": "Bật tất cả bảng định kiểu"
|
|
||||||
},
|
|
||||||
"disableStyleLabel": {
|
|
||||||
"message": "Vô hiệu hoá"
|
|
||||||
},
|
|
||||||
"draftAction": {
|
|
||||||
"message": "Chọn \"Có\" để tải bản nháp hoặc \"Không\" để xoá nó đi."
|
|
||||||
},
|
|
||||||
"editDeleteText": {
|
|
||||||
"message": "Xoá"
|
|
||||||
},
|
|
||||||
"editGotoLine": {
|
|
||||||
"message": "Đi đến dòng (hoặc dòng:cột)"
|
|
||||||
},
|
|
||||||
"editStyleHeading": {
|
|
||||||
"message": "Sửa bảng định kiểu"
|
|
||||||
},
|
|
||||||
"editStyleLabel": {
|
|
||||||
"message": "Sửa"
|
|
||||||
},
|
|
||||||
"editStyleTitle": {
|
|
||||||
"message": "Sửa bảng định kiểu $stylename$",
|
|
||||||
"placeholders": {
|
|
||||||
"stylename": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"editorSettings": {
|
|
||||||
"message": "Cài đặt trình soạn thảo"
|
|
||||||
},
|
|
||||||
"enableStyleLabel": {
|
|
||||||
"message": "Kích hoạt"
|
|
||||||
},
|
|
||||||
"exportCompatible": {
|
|
||||||
"message": "Xuất (chế độ tương thích)"
|
|
||||||
},
|
|
||||||
"exportLabel": {
|
|
||||||
"message": "Xuất"
|
|
||||||
},
|
|
||||||
"exportSavedSuccess": {
|
|
||||||
"message": "Lưu thành công"
|
|
||||||
},
|
|
||||||
"externalFeedback": {
|
|
||||||
"message": "Phản hồi"
|
|
||||||
},
|
|
||||||
"externalHomepage": {
|
|
||||||
"message": "Trang chủ"
|
|
||||||
},
|
|
||||||
"externalLink": {
|
|
||||||
"message": "Liên kết ngoài"
|
|
||||||
},
|
|
||||||
"externalSupport": {
|
|
||||||
"message": "Ủng hộ"
|
|
||||||
},
|
|
||||||
"genericAdd": {
|
|
||||||
"message": "Thêm"
|
|
||||||
},
|
|
||||||
"genericDescription": {
|
|
||||||
"message": "Mô tả"
|
|
||||||
},
|
|
||||||
"genericDisabledLabel": {
|
|
||||||
"message": "Vô hiệu hoá"
|
|
||||||
},
|
|
||||||
"genericEnabledLabel": {
|
|
||||||
"message": "Kích hoạt"
|
|
||||||
},
|
|
||||||
"genericError": {
|
|
||||||
"message": "Lỗi"
|
|
||||||
},
|
|
||||||
"genericHistoryLabel": {
|
|
||||||
"message": "Lịch sử"
|
|
||||||
},
|
|
||||||
"genericNext": {
|
|
||||||
"message": "Sau"
|
|
||||||
},
|
|
||||||
"genericPrevious": {
|
|
||||||
"message": "Trước"
|
|
||||||
},
|
|
||||||
"genericResetLabel": {
|
|
||||||
"message": "Đặt lại"
|
|
||||||
},
|
|
||||||
"genericSavedMessage": {
|
|
||||||
"message": "Đã lưu"
|
|
||||||
},
|
|
||||||
"genericTest": {
|
|
||||||
"message": "Thứ"
|
|
||||||
},
|
|
||||||
"genericTitle": {
|
|
||||||
"message": "Tiêu đề"
|
|
||||||
},
|
|
||||||
"genericUnknown": {
|
|
||||||
"message": "Không xác định"
|
|
||||||
},
|
|
||||||
"helpAlt": {
|
|
||||||
"message": "Trợ giúp"
|
|
||||||
},
|
|
||||||
"importLabel": {
|
|
||||||
"message": "Nhập"
|
|
||||||
},
|
|
||||||
"importReportLegendAdded": {
|
|
||||||
"message": "đã thêm"
|
|
||||||
},
|
|
||||||
"importReportLegendIdentical": {
|
|
||||||
"message": "Đã bỏ qua các tệp trùng lặp"
|
|
||||||
},
|
|
||||||
"importReportLegendInvalid": {
|
|
||||||
"message": "Đã bỏ qua các tệp không hợp lệ"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedBoth": {
|
|
||||||
"message": "đã cập nhật siêu thông tin và mã"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedCode": {
|
|
||||||
"message": "đã cập nhật mã"
|
|
||||||
},
|
|
||||||
"importReportLegendUpdatedMeta": {
|
|
||||||
"message": "đã cập nhật siêu thông tin"
|
|
||||||
},
|
|
||||||
"importReportTitle": {
|
|
||||||
"message": "Đã nhập xong"
|
|
||||||
},
|
|
||||||
"installUpdateFromLabel": {
|
|
||||||
"message": "Kiểm tra bản cập nhật mới"
|
|
||||||
},
|
|
||||||
"license": {
|
|
||||||
"message": "Giấy phép"
|
|
||||||
},
|
|
||||||
"linterCSSLintIncompatible": {
|
|
||||||
"message": "CSSLint không hỗ trợ bộ tiền xử lý $preprocessorname$",
|
|
||||||
"placeholders": {
|
|
||||||
"preprocessorname": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linterJSONError": {
|
|
||||||
"message": "Định dạng JSON không hợp lệ"
|
|
||||||
},
|
|
||||||
"styleEnabledLabel": {
|
|
||||||
"message": "Kích hoạt"
|
|
||||||
},
|
|
||||||
"styleSaveLabel": {
|
|
||||||
"message": "Lưu"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,26 +0,0 @@
|
||||||
/* global createWorkerApi */// worker-util.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/** @namespace BackgroundWorker */
|
|
||||||
createWorkerApi({
|
|
||||||
|
|
||||||
async compileUsercss(...args) {
|
|
||||||
require(['/js/usercss-compiler']); /* global compileUsercss */
|
|
||||||
return compileUsercss(...args);
|
|
||||||
},
|
|
||||||
|
|
||||||
nullifyInvalidVars(vars) {
|
|
||||||
require(['/js/meta-parser']); /* global metaParser */
|
|
||||||
return metaParser.nullifyInvalidVars(vars);
|
|
||||||
},
|
|
||||||
|
|
||||||
parseMozFormat(...args) {
|
|
||||||
require(['/js/moz-parser']); /* global extractSections */
|
|
||||||
return extractSections(...args);
|
|
||||||
},
|
|
||||||
|
|
||||||
parseUsercssMeta(text) {
|
|
||||||
require(['/js/meta-parser']);
|
|
||||||
return metaParser.parse(text);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,213 +1,462 @@
|
||||||
/* global API msg */// msg.js
|
/* global dbExec, getStyles, saveStyle */
|
||||||
/* global addAPI bgReady */// common.js
|
/* global handleCssTransitionBug */
|
||||||
/* global createWorker */// worker-util.js
|
/* global usercssHelper openEditor */
|
||||||
/* global prefs */
|
/* global styleViaAPI */
|
||||||
/* global styleMan */
|
|
||||||
/* global syncMan */
|
|
||||||
/* global updateMan */
|
|
||||||
/* global usercssMan */
|
|
||||||
/* global usoApi */
|
|
||||||
/* global uswApi */
|
|
||||||
/* global FIREFOX UA activateTab openURL */ // toolbox.js
|
|
||||||
/* global colorScheme */ // color-scheme.js
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
//#region API
|
// eslint-disable-next-line no-var
|
||||||
|
var browserCommands, contextMenus;
|
||||||
|
|
||||||
addAPI(/** @namespace API */ {
|
// *************************************************************************
|
||||||
|
// register all listeners
|
||||||
|
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||||
|
|
||||||
/** Temporary storage for data needed elsewhere e.g. in a content script */
|
{
|
||||||
data: ((data = {}) => ({
|
const [listener] = [
|
||||||
del: key => delete data[key],
|
[webNavigationListenerChrome, CHROME],
|
||||||
get: key => data[key],
|
[webNavigationListenerFF, FIREFOX],
|
||||||
has: key => key in data,
|
[webNavigationListener, true],
|
||||||
pop: key => {
|
].find(([, selected]) => selected);
|
||||||
const val = data[key];
|
|
||||||
delete data[key];
|
|
||||||
return val;
|
|
||||||
},
|
|
||||||
set: (key, val) => {
|
|
||||||
data[key] = val;
|
|
||||||
},
|
|
||||||
}))(),
|
|
||||||
|
|
||||||
styles: styleMan,
|
chrome.webNavigation.onBeforeNavigate.addListener(data =>
|
||||||
sync: syncMan,
|
listener(null, data));
|
||||||
updater: updateMan,
|
|
||||||
usercss: usercssMan,
|
|
||||||
uso: usoApi,
|
|
||||||
usw: uswApi,
|
|
||||||
colorScheme,
|
|
||||||
/** @type {BackgroundWorker} */
|
|
||||||
worker: createWorker({url: '/background/background-worker'}),
|
|
||||||
|
|
||||||
/** @returns {string} */
|
chrome.webNavigation.onCommitted.addListener(data =>
|
||||||
getTabUrlPrefix() {
|
listener('styleApply', data));
|
||||||
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||||
* Opens the editor or activates an existing tab
|
listener('styleReplaceAll', data));
|
||||||
* @param {{
|
|
||||||
id?: number
|
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||||
domain?: string
|
listener('styleReplaceAll', data));
|
||||||
'url-prefix'?: string
|
|
||||||
}} params
|
if (FIREFOX) {
|
||||||
* @returns {Promise<chrome.tabs.Tab>}
|
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
|
||||||
*/
|
chrome.webNavigation.onCommitted.addListener(webNavUsercssInstallerFF, {
|
||||||
async openEditor(params) {
|
url: [
|
||||||
const u = new URL(chrome.runtime.getURL('edit.html'));
|
{urlPrefix: 'https://raw.githubusercontent.com/', urlSuffix: '.user.css'},
|
||||||
u.search = new URLSearchParams(params);
|
{urlPrefix: 'https://raw.githubusercontent.com/', urlSuffix: '.user.styl'},
|
||||||
const wnd = chrome.windows && prefs.get('openEditInWindow');
|
]
|
||||||
const wndPos = wnd && prefs.get('windowPosition');
|
|
||||||
const wndBase = wnd && prefs.get('openEditInWindow.popup') ? {type: 'popup'} : {};
|
|
||||||
const ffBug = wnd && FIREFOX; // https://bugzil.la/1271047
|
|
||||||
if (wndPos) {
|
|
||||||
const {left, top, width, height} = wndPos;
|
|
||||||
const r = left + width;
|
|
||||||
const b = top + height;
|
|
||||||
const peek = 32;
|
|
||||||
if (isNaN(r) || r < peek || left > screen.availWidth - peek || width < 100) {
|
|
||||||
delete wndPos.left;
|
|
||||||
delete wndPos.width;
|
|
||||||
}
|
|
||||||
if (isNaN(b) || b < peek || top > screen.availHeight - peek || height < 100) {
|
|
||||||
delete wndPos.top;
|
|
||||||
delete wndPos.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tab = await openURL({
|
|
||||||
url: `${u}`,
|
|
||||||
currentWindow: null,
|
|
||||||
newWindow: wnd && Object.assign(wndBase, !ffBug && wndPos),
|
|
||||||
});
|
});
|
||||||
if (ffBug) await browser.windows.update(tab.windowId, wndPos);
|
}
|
||||||
return tab;
|
}
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<chrome.tabs.Tab>} */
|
if (chrome.contextMenus) {
|
||||||
async openManage({options = false, search, searchMode} = {}) {
|
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
||||||
const setUrlParams = url => {
|
contextMenus[info.menuItemId].click(info, tab));
|
||||||
const u = new URL(url);
|
}
|
||||||
if (search) u.searchParams.set('search', search);
|
if (chrome.commands) {
|
||||||
if (searchMode) u.searchParams.set('searchMode', searchMode);
|
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
|
||||||
if (options) u.hash = '#stylus-options';
|
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||||
return u.href;
|
}
|
||||||
};
|
|
||||||
const base = chrome.runtime.getURL('manage.html');
|
if (!chrome.browserAction ||
|
||||||
const url = setUrlParams(base);
|
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
|
||||||
const tabs = await browser.tabs.query({url: base + '*'});
|
window.updateIcon = () => {};
|
||||||
const same = tabs.find(t => t.url === url);
|
}
|
||||||
let tab = same || tabs[0];
|
|
||||||
if (!tab) {
|
// *************************************************************************
|
||||||
API.prefsDb.get('badFavs'); // prime the cache to avoid flicker/delay when opening the page
|
// set the default icon displayed after a tab is created until webNavigation kicks in
|
||||||
tab = await openURL({url, newTab: true});
|
prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {}));
|
||||||
} else if (!same) {
|
|
||||||
msg.sendTab(tab.id, {method: 'pushState', url: setUrlParams(tab.url)});
|
// *************************************************************************
|
||||||
|
{
|
||||||
|
const onInstall = ({reason}) => {
|
||||||
|
chrome.runtime.onInstalled.removeListener(onInstall);
|
||||||
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
// Open FAQs page once after installation to guide new users.
|
||||||
|
// Do not display it in development mode.
|
||||||
|
if (reason === 'install' && manifest.update_url) {
|
||||||
|
// don't hardcode homepage URL, extract it from "Get Help" label translation
|
||||||
|
// TODO: add a built-in tour page in the extension
|
||||||
|
const getHelpHtml = chrome.i18n.getMessage('manageText').match(/<a\s+href=[^>]+/g);
|
||||||
|
const url = (getHelpHtml[1] || '').replace(/^.+?=\s*/, '').replace(/^['"]|["']$/g, '');
|
||||||
|
if (url) {
|
||||||
|
setTimeout(openURL, 100, {url});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return activateTab(tab); // activateTab unminimizes the window
|
// reset L10N cache on update
|
||||||
},
|
if (reason === 'update') {
|
||||||
|
localStorage.L10N = JSON.stringify({
|
||||||
/**
|
browserUIlanguage: chrome.i18n.getUILanguage(),
|
||||||
* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
|
});
|
||||||
* when the tab is ready, which is needed in the popup, otherwise another
|
|
||||||
* extension could force the tab to open in foreground thus auto-closing the
|
|
||||||
* popup (in Chrome at least) and preventing the sendMessage code from running
|
|
||||||
* @returns {Promise<chrome.tabs.Tab>}
|
|
||||||
*/
|
|
||||||
async openURL(opts) {
|
|
||||||
const tab = await openURL(opts);
|
|
||||||
if (opts.message) {
|
|
||||||
await onTabReady(tab);
|
|
||||||
await msg.sendTab(tab.id, opts.message);
|
|
||||||
}
|
}
|
||||||
return tab;
|
if (!FIREFOX && chrome.declarativeContent) {
|
||||||
function onTabReady(tab) {
|
chrome.declarativeContent.onPageChanged.removeRules(null, () => {
|
||||||
return new Promise((resolve, reject) =>
|
chrome.declarativeContent.onPageChanged.addRules([{
|
||||||
setTimeout(function ping(numTries = 10, delay = 100) {
|
conditions: [
|
||||||
msg.sendTab(tab.id, {method: 'ping'})
|
new chrome.declarativeContent.PageStateMatcher({
|
||||||
.catch(() => false)
|
pageUrl: {urlContains: ':'},
|
||||||
.then(pong => pong
|
})
|
||||||
? resolve(tab)
|
],
|
||||||
: numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
|
actions: [
|
||||||
reject('timeout'));
|
new chrome.declarativeContent.RequestContentScript({
|
||||||
}));
|
js: ['/content/apply.js'],
|
||||||
|
allFrames: true,
|
||||||
|
matchAboutBlank: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
// bind for 60 seconds max and auto-unbind if it's a normal run
|
||||||
|
chrome.runtime.onInstalled.addListener(onInstall);
|
||||||
|
setTimeout(onInstall, 60e3, {reason: 'unbindme'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************
|
||||||
|
// browser commands
|
||||||
|
browserCommands = {
|
||||||
|
openManage() {
|
||||||
|
openURL({url: 'manage.html'});
|
||||||
},
|
},
|
||||||
|
|
||||||
prefs: {
|
|
||||||
getValues: () => prefs.__values, // will be deepCopy'd by apiHandler
|
|
||||||
set: prefs.set,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Events
|
|
||||||
|
|
||||||
const browserCommands = {
|
|
||||||
openManage: () => API.openManage(),
|
|
||||||
openOptions: () => API.openManage({options: true}),
|
|
||||||
reload: () => chrome.runtime.reload(),
|
|
||||||
styleDisableAll(info) {
|
styleDisableAll(info) {
|
||||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (chrome.commands) {
|
// *************************************************************************
|
||||||
chrome.commands.onCommand.addListener(id => browserCommands[id]());
|
// context menus
|
||||||
|
contextMenus = {
|
||||||
|
'show-badge': {
|
||||||
|
title: 'menuShowBadge',
|
||||||
|
click: info => prefs.set(info.menuItemId, info.checked),
|
||||||
|
},
|
||||||
|
'disableAll': {
|
||||||
|
title: 'disableAllStyles',
|
||||||
|
click: browserCommands.styleDisableAll,
|
||||||
|
},
|
||||||
|
'open-manager': {
|
||||||
|
title: 'openStylesManager',
|
||||||
|
click: browserCommands.openManage,
|
||||||
|
},
|
||||||
|
'editor.contextDelete': {
|
||||||
|
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
|
||||||
|
title: 'editDeleteText',
|
||||||
|
type: 'normal',
|
||||||
|
contexts: ['editable'],
|
||||||
|
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
||||||
|
click: (info, tab) => {
|
||||||
|
sendMessage({tabId: tab.id, method: 'editDeleteText'});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (chrome.contextMenus) {
|
||||||
|
const createContextMenus = ids => {
|
||||||
|
for (const id of ids) {
|
||||||
|
let item = contextMenus[id];
|
||||||
|
if (item.presentIf && !item.presentIf()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
item = Object.assign({id}, item);
|
||||||
|
delete item.presentIf;
|
||||||
|
const prefValue = prefs.readOnlyValues[id];
|
||||||
|
item.title = chrome.i18n.getMessage(item.title);
|
||||||
|
if (!item.type && typeof prefValue === 'boolean') {
|
||||||
|
item.type = 'checkbox';
|
||||||
|
item.checked = prefValue;
|
||||||
|
}
|
||||||
|
if (!item.contexts) {
|
||||||
|
item.contexts = ['browser_action'];
|
||||||
|
}
|
||||||
|
delete item.click;
|
||||||
|
chrome.contextMenus.create(item, ignoreChromeError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// circumvent the bug with disabling check marks in Chrome 62+
|
||||||
|
// TODO: replace 1e6 with the actual rev. number when/if the bug is fixed
|
||||||
|
const toggleCheckmark = CHROME >= 3172 && CHROME <= 1e6 ?
|
||||||
|
(id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) :
|
||||||
|
((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError));
|
||||||
|
|
||||||
|
const togglePresence = (id, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
createContextMenus([id]);
|
||||||
|
} else {
|
||||||
|
chrome.contextMenus.remove(id, ignoreChromeError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys = Object.keys(contextMenus);
|
||||||
|
prefs.subscribe(keys.filter(id => typeof prefs.readOnlyValues[id] === 'boolean'), toggleCheckmark);
|
||||||
|
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
|
||||||
|
createContextMenus(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
// *************************************************************************
|
||||||
if (reason === 'install') {
|
// [re]inject content scripts
|
||||||
if (UA.mobile) prefs.set('manage.newUI', false);
|
window.addEventListener('storageReady', function _() {
|
||||||
if (UA.windows) prefs.set('editor.keyMap', 'sublime');
|
window.removeEventListener('storageReady', _);
|
||||||
|
|
||||||
|
updateIcon({id: undefined}, {});
|
||||||
|
|
||||||
|
if (FIREFOX) {
|
||||||
|
queryTabs().then(tabs =>
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
if (!tab.width) {
|
||||||
|
// skip lazy-loaded tabs (width = 0) that seem to start loading on message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tabId = tab.id;
|
||||||
|
const frameUrls = {0: tab.url};
|
||||||
|
styleViaAPI.allFrameUrls.set(tabId, frameUrls);
|
||||||
|
chrome.webNavigation.getAllFrames({tabId}, frames => frames &&
|
||||||
|
frames.forEach(({frameId, parentFrameId, url}) => {
|
||||||
|
if (frameId) {
|
||||||
|
frameUrls[frameId] = url === 'about:blank' ? frameUrls[parentFrameId] : url;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// TODO: remove this before 1.5.23 as it's only for a few users who installed git 26b75e77
|
|
||||||
if (reason === 'update' && previousVersion === '1.5.22') {
|
const NTP = 'chrome://newtab/';
|
||||||
for (const dbName of ['drafts', prefs.STORAGE_KEY]) {
|
const ALL_URLS = '<all_urls>';
|
||||||
try {
|
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||||
indexedDB.open(dbName).onsuccess = async e => {
|
contentScripts.push({
|
||||||
const idb = /** @type IDBDatabase */ e.target.result;
|
js: ['content/apply.js'],
|
||||||
const ta = idb.objectStoreNames[0] === 'data' && idb.transaction(['data']);
|
matches: ['<all_urls>'],
|
||||||
if (ta && ta.objectStore('data').autoIncrement) {
|
run_at: 'document_start',
|
||||||
ta.abort();
|
match_about_blank: true,
|
||||||
idb.close();
|
all_frames: true
|
||||||
await new Promise(setTimeout);
|
});
|
||||||
indexedDB.deleteDatabase(dbName);
|
|
||||||
}
|
// expand * as .*?
|
||||||
};
|
const wildcardAsRegExp = (s, flags) => new RegExp(
|
||||||
} catch (e) {}
|
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
|
||||||
|
.replace(/\*/g, '.*?'), flags);
|
||||||
|
for (const cs of contentScripts) {
|
||||||
|
cs.matches = cs.matches.map(m => (
|
||||||
|
m === ALL_URLS ? m : wildcardAsRegExp(m)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectCS = (cs, tabId) => {
|
||||||
|
chrome.tabs.executeScript(tabId, {
|
||||||
|
file: cs.js[0],
|
||||||
|
runAt: cs.run_at,
|
||||||
|
allFrames: cs.all_frames,
|
||||||
|
matchAboutBlank: cs.match_about_blank,
|
||||||
|
}, ignoreChromeError);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pingCS = (cs, {id, url}) => {
|
||||||
|
const maybeInject = pong => !pong && injectCS(cs, id);
|
||||||
|
cs.matches.some(match => {
|
||||||
|
if ((match === ALL_URLS || url.match(match)) &&
|
||||||
|
(!url.startsWith('chrome') || url === NTP)) {
|
||||||
|
sendMessage({method: 'ping', tabId: id}, maybeInject);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queryTabs().then(tabs =>
|
||||||
|
tabs.forEach(tab => tab.width &&
|
||||||
|
contentScripts.forEach(cs =>
|
||||||
|
setTimeout(pingCS, 0, cs, tab))));
|
||||||
|
});
|
||||||
|
|
||||||
|
// *************************************************************************
|
||||||
|
|
||||||
|
function webNavigationListener(method, {url, tabId, frameId}) {
|
||||||
|
getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => {
|
||||||
|
if (method && URLS.supported(url) && tabId >= 0) {
|
||||||
|
if (method === 'styleApply') {
|
||||||
|
handleCssTransitionBug({tabId, frameId, url, styles});
|
||||||
|
}
|
||||||
|
sendMessage({
|
||||||
|
tabId,
|
||||||
|
frameId,
|
||||||
|
method,
|
||||||
|
// ping own page so it retrieves the styles directly
|
||||||
|
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// main page frame id is 0
|
||||||
|
if (frameId === 0) {
|
||||||
|
updateIcon({id: tabId, url}, styles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function webNavigationListenerChrome(method, data) {
|
||||||
|
const {tabId, frameId, url} = data;
|
||||||
|
if (url.startsWith('https://www.google.') && url.includes('/_/chrome/newtab?')) {
|
||||||
|
// Chrome 61.0.3161+ doesn't run content scripts on NTP
|
||||||
|
getTab(tabId).then(tab => {
|
||||||
|
data.url = tab.url === 'chrome://newtab/' ? tab.url : url;
|
||||||
|
webNavigationListener(method, data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
webNavigationListener(method, data);
|
||||||
|
// chrome.declarativeContent doesn't inject scripts in about:blank iframes
|
||||||
|
if (method && frameId && url === 'about:blank') {
|
||||||
|
chrome.tabs.executeScript(tabId, {
|
||||||
|
file: '/content/apply.js',
|
||||||
|
runAt: 'document_start',
|
||||||
|
matchAboutBlank: true,
|
||||||
|
frameId,
|
||||||
|
}, ignoreChromeError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
msg.on((msg, sender) => {
|
|
||||||
if (msg.method === 'invokeAPI') {
|
function webNavigationListenerFF(method, data) {
|
||||||
let res = msg.path.reduce((res, name) => res && res[name], API);
|
const {tabId, frameId, url} = data;
|
||||||
if (!res) throw new Error(`Unknown API.${msg.path.join('.')}`);
|
//console.log(method, data);
|
||||||
res = res.apply({msg, sender}, msg.args);
|
if (frameId === 0 || url !== 'about:blank') {
|
||||||
return res === undefined ? null : res;
|
if ((!method || method === 'styleApply') &&
|
||||||
|
styleViaAPI.getFrameUrl(tabId, frameId) !== url) {
|
||||||
|
styleViaAPI.cache.delete(tabId);
|
||||||
|
}
|
||||||
|
styleViaAPI.setFrameUrl(tabId, frameId, url);
|
||||||
|
webNavigationListener(method, data);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
//const frames = styleViaAPI.allFrameUrls.get(tabId);
|
||||||
|
//if (Object.keys(frames).length === 1) {
|
||||||
|
// frames[frameId] = frames['0'];
|
||||||
|
// webNavigationListener(method, data);
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
//chrome.webNavigation.getFrame({tabId, frameId}, info => {
|
||||||
|
// const hasParent = !chrome.runtime.lastError && info.parentFrameId >= 0;
|
||||||
|
// frames[frameId] = hasParent ? frames[info.parentFrameId] : url;
|
||||||
|
// webNavigationListener(method, data);
|
||||||
|
//});
|
||||||
|
}
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
Promise.all([
|
function webNavUsercssInstallerFF(data) {
|
||||||
browser.extension.isAllowedFileSchemeAccess()
|
const {tabId} = data;
|
||||||
.then(res => API.data.set('hasFileAccess', res)),
|
// we need tab index to open the installer next to the original one
|
||||||
bgReady.styles,
|
// and also to skip the double-invocation in FF which assigns tab url later
|
||||||
/* These are loaded conditionally.
|
getTab(tabId).then(tab => {
|
||||||
Each item uses `require` individually so IDE can jump to the source and track usage. */
|
if (tab.url !== 'about:blank') {
|
||||||
FIREFOX &&
|
usercssHelper.openInstallPage(tab, {direct: true});
|
||||||
require(['/background/style-via-api']),
|
}
|
||||||
FIREFOX && ((browser.commands || {}).update) &&
|
});
|
||||||
require(['/background/browser-cmd-hotkeys']),
|
}
|
||||||
!FIREFOX &&
|
|
||||||
require(['/background/content-scripts']),
|
|
||||||
chrome.contextMenus &&
|
function updateIcon(tab, styles) {
|
||||||
require(['/background/context-menus']),
|
if (tab.id < 0) {
|
||||||
]).then(() => {
|
return;
|
||||||
bgReady._resolveAll();
|
}
|
||||||
msg.ready = true;
|
if (URLS.chromeProtectsNTP && tab.url === 'chrome://newtab/') {
|
||||||
msg.broadcast({method: 'backgroundReady'});
|
styles = {};
|
||||||
});
|
}
|
||||||
|
if (styles) {
|
||||||
|
stylesReceived(styles);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getTabRealURL(tab)
|
||||||
|
.then(url => getStyles({matchUrl: url, enabled: true, asHash: true}))
|
||||||
|
.then(stylesReceived);
|
||||||
|
|
||||||
|
function stylesReceived(styles) {
|
||||||
|
let numStyles = styles.length;
|
||||||
|
if (numStyles === undefined) {
|
||||||
|
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||||
|
numStyles = 0;
|
||||||
|
for (const id of Object.keys(styles)) {
|
||||||
|
numStyles += id.match(/^\d+$/) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||||
|
const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : '';
|
||||||
|
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||||
|
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||||
|
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
||||||
|
const path = 'images/icon/' + iconset;
|
||||||
|
chrome.browserAction.setIcon({
|
||||||
|
tabId: tab.id,
|
||||||
|
path: {
|
||||||
|
// Material Design 2016 new size is 16px
|
||||||
|
16: `${path}16${postfix}.png`,
|
||||||
|
32: `${path}32${postfix}.png`,
|
||||||
|
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||||
|
19: `${path}19${postfix}.png`,
|
||||||
|
38: `${path}38${postfix}.png`,
|
||||||
|
// TODO: add Edge preferred sizes: 20, 25, 30, 40
|
||||||
|
},
|
||||||
|
}, () => {
|
||||||
|
if (chrome.runtime.lastError || tab.id === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||||
|
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||||
|
setTimeout(() => {
|
||||||
|
getTab(tab.id).then(realTab => {
|
||||||
|
// skip pre-rendered tabs
|
||||||
|
if (realTab.index >= 0) {
|
||||||
|
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onRuntimeMessage(request, sender, sendResponseInternal) {
|
||||||
|
const sendResponse = data => {
|
||||||
|
// wrap Error object instance as {__ERROR__: message} - will be unwrapped in sendMessage
|
||||||
|
if (data instanceof Error) {
|
||||||
|
data = {__ERROR__: data.message};
|
||||||
|
}
|
||||||
|
// prevent browser exception bug on sending a response to a closed tab
|
||||||
|
tryCatch(sendResponseInternal, data);
|
||||||
|
};
|
||||||
|
switch (request.method) {
|
||||||
|
case 'getStyles':
|
||||||
|
getStyles(request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'saveStyle':
|
||||||
|
saveStyle(request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'saveUsercss':
|
||||||
|
usercssHelper.save(request, true).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'buildUsercss':
|
||||||
|
usercssHelper.build(request, true).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'healthCheck':
|
||||||
|
dbExec()
|
||||||
|
.then(() => sendResponse(true))
|
||||||
|
.catch(() => sendResponse(false));
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'download':
|
||||||
|
download(request.url)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch(() => sendResponse(null));
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'openUsercssInstallPage':
|
||||||
|
usercssHelper.openInstallPage(sender.tab, request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'closeTab':
|
||||||
|
chrome.tabs.remove(request.tabId || sender.tab.id, () => {
|
||||||
|
if (chrome.runtime.lastError && request.tabId !== sender.tab.id) {
|
||||||
|
sendResponse(new Error(chrome.runtime.lastError.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'openEditor':
|
||||||
|
openEditor(request.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Registers hotkeys in FF
|
|
||||||
*/
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const hotkeyPrefs = prefs.knownKeys.filter(k => k.startsWith('hotkey.'));
|
|
||||||
prefs.subscribe(hotkeyPrefs, updateHotkey, {runNow: true});
|
|
||||||
|
|
||||||
async function updateHotkey(name, value) {
|
|
||||||
try {
|
|
||||||
name = name.split('.')[1];
|
|
||||||
if (value.trim()) {
|
|
||||||
await browser.commands.update({name, shortcut: value});
|
|
||||||
} else {
|
|
||||||
await browser.commands.reset(name);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,91 +0,0 @@
|
||||||
/* global prefs */
|
|
||||||
/* exported colorScheme */
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const colorScheme = (() => {
|
|
||||||
const changeListeners = new Set();
|
|
||||||
const kSTATE = 'schemeSwitcher.enabled';
|
|
||||||
const kSTART = 'schemeSwitcher.nightStart';
|
|
||||||
const kEND = 'schemeSwitcher.nightEnd';
|
|
||||||
const SCHEMES = ['dark', 'light'];
|
|
||||||
const isDark = {
|
|
||||||
never: null,
|
|
||||||
dark: true,
|
|
||||||
light: false,
|
|
||||||
system: false,
|
|
||||||
time: false,
|
|
||||||
};
|
|
||||||
let isDarkNow = false;
|
|
||||||
|
|
||||||
prefs.subscribe(kSTATE, () => update());
|
|
||||||
prefs.subscribe([kSTART, kEND], (key, value) => {
|
|
||||||
updateTimePreferDark();
|
|
||||||
createAlarm(key, value);
|
|
||||||
}, {runNow: true});
|
|
||||||
chrome.alarms.onAlarm.addListener(({name}) => {
|
|
||||||
if (name === kSTART || name === kEND) {
|
|
||||||
updateTimePreferDark();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
SCHEMES,
|
|
||||||
onChange(listener) {
|
|
||||||
changeListeners.add(listener);
|
|
||||||
},
|
|
||||||
isDark: () => isDarkNow,
|
|
||||||
/** @param {StyleObj} _ */
|
|
||||||
shouldIncludeStyle({preferScheme: ps}) {
|
|
||||||
return prefs.get(kSTATE) === 'never' ||
|
|
||||||
!SCHEMES.includes(ps) ||
|
|
||||||
isDarkNow === (ps === 'dark');
|
|
||||||
},
|
|
||||||
updateSystemPreferDark(val) {
|
|
||||||
update('system', val);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function calcTime(key) {
|
|
||||||
const [h, m] = prefs.get(key).split(':');
|
|
||||||
return (h * 3600 + m * 60) * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAlarm(key, value) {
|
|
||||||
const date = new Date();
|
|
||||||
const [h, m] = value.split(':');
|
|
||||||
date.setHours(h, m, 0, 0);
|
|
||||||
if (date.getTime() < Date.now()) {
|
|
||||||
date.setDate(date.getDate() + 1);
|
|
||||||
}
|
|
||||||
chrome.alarms.create(key, {
|
|
||||||
when: date.getTime(),
|
|
||||||
periodInMinutes: 24 * 60,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTimePreferDark() {
|
|
||||||
const now = Date.now() - new Date().setHours(0, 0, 0, 0);
|
|
||||||
const start = calcTime(kSTART);
|
|
||||||
const end = calcTime(kEND);
|
|
||||||
const val = start > end ?
|
|
||||||
now >= start || now < end :
|
|
||||||
now >= start && now < end;
|
|
||||||
update('time', val);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(type, val) {
|
|
||||||
if (type) {
|
|
||||||
if (isDark[type] === val) return;
|
|
||||||
isDark[type] = val;
|
|
||||||
}
|
|
||||||
val = isDark[prefs.get(kSTATE)];
|
|
||||||
if (isDarkNow !== val) {
|
|
||||||
isDarkNow = val;
|
|
||||||
for (const listener of changeListeners) {
|
|
||||||
listener(isDarkNow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,92 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common stuff that's loaded first so it's immediately available to all background scripts
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.bgReady = {}; /* global bgReady */
|
|
||||||
bgReady.styles = new Promise(r => (bgReady._resolveStyles = r));
|
|
||||||
bgReady.all = new Promise(r => (bgReady._resolveAll = r));
|
|
||||||
|
|
||||||
const uuidIndex = Object.assign(new Map(), {
|
|
||||||
custom: {},
|
|
||||||
/** `obj` must have a unique `id`, a UUIDv4 `_id`, and Date.now() for `_rev`. */
|
|
||||||
addCustomId(obj, {get = () => obj, set}) {
|
|
||||||
Object.defineProperty(uuidIndex.custom, obj.id, {get, set});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* exported addAPI */
|
|
||||||
function addAPI(methods) {
|
|
||||||
for (const [key, val] of Object.entries(methods)) {
|
|
||||||
const old = API[key];
|
|
||||||
if (old && Object.prototype.toString.call(old) === '[object Object]') {
|
|
||||||
Object.assign(old, val);
|
|
||||||
} else {
|
|
||||||
API[key] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* exported createCache */
|
|
||||||
/** Creates a FIFO limit-size map. */
|
|
||||||
function createCache({size = 1000, onDeleted} = {}) {
|
|
||||||
const map = new Map();
|
|
||||||
const buffer = Array(size);
|
|
||||||
let index = 0;
|
|
||||||
let lastIndex = 0;
|
|
||||||
return {
|
|
||||||
get(id) {
|
|
||||||
const item = map.get(id);
|
|
||||||
return item && item.data;
|
|
||||||
},
|
|
||||||
set(id, data) {
|
|
||||||
if (map.size === size) {
|
|
||||||
// full
|
|
||||||
map.delete(buffer[lastIndex].id);
|
|
||||||
if (onDeleted) {
|
|
||||||
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
|
|
||||||
}
|
|
||||||
lastIndex = (lastIndex + 1) % size;
|
|
||||||
}
|
|
||||||
const item = {id, data, index};
|
|
||||||
map.set(id, item);
|
|
||||||
buffer[index] = item;
|
|
||||||
index = (index + 1) % size;
|
|
||||||
},
|
|
||||||
delete(id) {
|
|
||||||
const item = map.get(id);
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
map.delete(item.id);
|
|
||||||
const lastItem = buffer[lastIndex];
|
|
||||||
lastItem.index = item.index;
|
|
||||||
buffer[item.index] = lastItem;
|
|
||||||
lastIndex = (lastIndex + 1) % size;
|
|
||||||
if (onDeleted) {
|
|
||||||
onDeleted(item.id, item.data);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
map.clear();
|
|
||||||
index = lastIndex = 0;
|
|
||||||
},
|
|
||||||
has: id => map.has(id),
|
|
||||||
*entries() {
|
|
||||||
for (const [id, item] of map) {
|
|
||||||
yield [id, item.data];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
*values() {
|
|
||||||
for (const item of map.values()) {
|
|
||||||
yield item.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get size() {
|
|
||||||
return map.size;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
/* global bgReady */// common.js
|
|
||||||
/* global msg */
|
|
||||||
/* global URLS ignoreChromeError */// toolbox.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Reinject content scripts when the extension is reloaded/updated.
|
|
||||||
Not used in Firefox as it reinjects automatically.
|
|
||||||
*/
|
|
||||||
|
|
||||||
bgReady.all.then(() => {
|
|
||||||
const NTP = 'chrome://newtab/';
|
|
||||||
const ALL_URLS = '<all_urls>';
|
|
||||||
const SCRIPTS = chrome.runtime.getManifest().content_scripts;
|
|
||||||
// expand * as .*?
|
|
||||||
const wildcardAsRegExp = (s, flags) => new RegExp(
|
|
||||||
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
|
|
||||||
.replace(/\*/g, '.*?'), flags);
|
|
||||||
for (const cs of SCRIPTS) {
|
|
||||||
cs.matches = cs.matches.map(m => (
|
|
||||||
m === ALL_URLS ? m : wildcardAsRegExp(m)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
const busyTabs = new Set();
|
|
||||||
let busyTabsTimer;
|
|
||||||
|
|
||||||
setTimeout(injectToAllTabs);
|
|
||||||
|
|
||||||
function injectToTab({url, tabId, frameId = null}) {
|
|
||||||
for (const script of SCRIPTS) {
|
|
||||||
if (
|
|
||||||
script.matches.some(match =>
|
|
||||||
(match === ALL_URLS || url.match(match)) &&
|
|
||||||
(!url.startsWith('chrome') || url === NTP))
|
|
||||||
) {
|
|
||||||
doInject(tabId, frameId, script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doInject(tabId, frameId, script) {
|
|
||||||
const options = frameId === null ? {} : {frameId};
|
|
||||||
msg.sendTab(tabId, {method: 'ping'}, options)
|
|
||||||
.catch(() => false)
|
|
||||||
.then(pong => {
|
|
||||||
if (pong) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = {
|
|
||||||
runAt: script.run_at,
|
|
||||||
allFrames: script.all_frames,
|
|
||||||
matchAboutBlank: script.match_about_blank,
|
|
||||||
};
|
|
||||||
if (frameId !== null) {
|
|
||||||
options.allFrames = false;
|
|
||||||
options.frameId = frameId;
|
|
||||||
}
|
|
||||||
for (const file of script.js) {
|
|
||||||
chrome.tabs.executeScript(tabId, Object.assign({file}, options), ignoreChromeError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectToAllTabs() {
|
|
||||||
return browser.tabs.query({}).then(tabs => {
|
|
||||||
for (const tab of tabs) {
|
|
||||||
// skip unloaded/discarded/chrome tabs
|
|
||||||
if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue;
|
|
||||||
// our content scripts may still be pending injection at browser start so it's too early to ping them
|
|
||||||
if (tab.status === 'loading') {
|
|
||||||
trackBusyTab(tab.id, true);
|
|
||||||
} else {
|
|
||||||
injectToTab({
|
|
||||||
url: tab.pendingUrl || tab.url,
|
|
||||||
tabId: tab.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleBusyTabListeners(state) {
|
|
||||||
const toggle = state ? 'addListener' : 'removeListener';
|
|
||||||
chrome.webNavigation.onCompleted[toggle](onBusyTabUpdated);
|
|
||||||
chrome.webNavigation.onErrorOccurred[toggle](onBusyTabUpdated);
|
|
||||||
chrome.webNavigation.onTabReplaced[toggle](onBusyTabReplaced);
|
|
||||||
chrome.tabs.onRemoved[toggle](onBusyTabRemoved);
|
|
||||||
if (state) {
|
|
||||||
busyTabsTimer = setTimeout(toggleBusyTabListeners, 15e3, false);
|
|
||||||
} else {
|
|
||||||
clearTimeout(busyTabsTimer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackBusyTab(tabId, state) {
|
|
||||||
busyTabs[state ? 'add' : 'delete'](tabId);
|
|
||||||
if (state && busyTabs.size === 1) toggleBusyTabListeners(true);
|
|
||||||
if (!state && !busyTabs.size) toggleBusyTabListeners(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBusyTabUpdated({error, frameId, tabId, url}) {
|
|
||||||
if (!frameId && busyTabs.has(tabId)) {
|
|
||||||
trackBusyTab(tabId, false);
|
|
||||||
if (url && !error) {
|
|
||||||
injectToTab({tabId, url});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBusyTabReplaced({replacedTabId}) {
|
|
||||||
trackBusyTab(replacedTabId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBusyTabRemoved(tabId) {
|
|
||||||
trackBusyTab(tabId, false);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,87 +0,0 @@
|
||||||
/* global browserCommands */// background.js
|
|
||||||
/* global msg */
|
|
||||||
/* global prefs */
|
|
||||||
/* global CHROME URLS ignoreChromeError */// toolbox.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.management.getSelf(ext => {
|
|
||||||
const contextMenus = Object.assign({
|
|
||||||
'show-badge': {
|
|
||||||
title: 'menuShowBadge',
|
|
||||||
click: togglePref,
|
|
||||||
},
|
|
||||||
'disableAll': {
|
|
||||||
title: 'disableAllStyles',
|
|
||||||
click: browserCommands.styleDisableAll,
|
|
||||||
},
|
|
||||||
'open-manager': {
|
|
||||||
title: 'optionsOpenManager',
|
|
||||||
click: browserCommands.openManage,
|
|
||||||
},
|
|
||||||
'open-options': {
|
|
||||||
title: 'openOptions',
|
|
||||||
click: browserCommands.openOptions,
|
|
||||||
},
|
|
||||||
}, ext.installType === 'development' && {
|
|
||||||
'reload': {
|
|
||||||
title: 'reload',
|
|
||||||
click: browserCommands.reload,
|
|
||||||
},
|
|
||||||
}, CHROME && {
|
|
||||||
'editor.contextDelete': {
|
|
||||||
title: 'editDeleteText',
|
|
||||||
type: 'normal',
|
|
||||||
contexts: ['editable'],
|
|
||||||
documentUrlPatterns: [URLS.ownOrigin + '*'],
|
|
||||||
click: (info, tab) => {
|
|
||||||
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
|
|
||||||
.catch(msg.ignoreError);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createContextMenus(Object.keys(contextMenus));
|
|
||||||
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
|
||||||
contextMenus[info.menuItemId].click(info, tab));
|
|
||||||
|
|
||||||
function createContextMenus(ids) {
|
|
||||||
for (const id of ids) {
|
|
||||||
const item = Object.assign({id, contexts: ['browser_action']}, contextMenus[id]);
|
|
||||||
item.title = chrome.i18n.getMessage(item.title);
|
|
||||||
if (typeof prefs.defaults[id] === 'boolean') {
|
|
||||||
if (item.type) {
|
|
||||||
prefs.subscribe(id, togglePresence);
|
|
||||||
} else {
|
|
||||||
item.type = 'checkbox';
|
|
||||||
item.checked = prefs.get(id);
|
|
||||||
prefs.subscribe(id, CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete item.click;
|
|
||||||
chrome.contextMenus.create(item, ignoreChromeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleCheckmark(id, checked) {
|
|
||||||
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Circumvents the bug with disabling check marks in Chrome 62-64 */
|
|
||||||
async function toggleCheckmarkBugged(id) {
|
|
||||||
await browser.contextMenus.remove(id).catch(ignoreChromeError);
|
|
||||||
createContextMenus([id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.contextMenus.OnClickData} info */
|
|
||||||
function togglePref(info) {
|
|
||||||
prefs.set(info.menuItemId, info.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePresence(id, checked) {
|
|
||||||
if (checked) {
|
|
||||||
createContextMenus([id]);
|
|
||||||
} else {
|
|
||||||
chrome.contextMenus.remove(id, ignoreChromeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,64 +0,0 @@
|
||||||
/* global chromeLocal */// storage-util.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* exported createChromeStorageDB */
|
|
||||||
function createChromeStorageDB(PREFIX) {
|
|
||||||
let INC;
|
|
||||||
const isMain = !PREFIX;
|
|
||||||
if (!PREFIX) PREFIX = 'style-';
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
delete(id) {
|
|
||||||
return chromeLocal.remove(PREFIX + id);
|
|
||||||
},
|
|
||||||
|
|
||||||
get(id) {
|
|
||||||
return chromeLocal.getValue(PREFIX + id);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getAll() {
|
|
||||||
const all = await chromeLocal.get();
|
|
||||||
if (!INC) prepareInc(all);
|
|
||||||
return Object.entries(all)
|
|
||||||
.map(([key, val]) => key.startsWith(PREFIX) &&
|
|
||||||
(!isMain || Number(key.slice(PREFIX.length))) &&
|
|
||||||
val)
|
|
||||||
.filter(Boolean);
|
|
||||||
},
|
|
||||||
|
|
||||||
async put(item) {
|
|
||||||
if (!item.id) {
|
|
||||||
if (!INC) await prepareInc();
|
|
||||||
item.id = INC++;
|
|
||||||
}
|
|
||||||
await chromeLocal.setValue(PREFIX + item.id, item);
|
|
||||||
return item.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
async putMany(items) {
|
|
||||||
const data = {};
|
|
||||||
for (const item of items) {
|
|
||||||
if (!item.id) {
|
|
||||||
if (!INC) await prepareInc();
|
|
||||||
item.id = INC++;
|
|
||||||
}
|
|
||||||
data[PREFIX + item.id] = item;
|
|
||||||
}
|
|
||||||
await chromeLocal.set(data);
|
|
||||||
return items.map(_ => _.id);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function prepareInc(data) {
|
|
||||||
INC = 1;
|
|
||||||
for (const key in data || await chromeLocal.get()) {
|
|
||||||
if (key.startsWith(PREFIX)) {
|
|
||||||
const id = Number(key.slice(PREFIX.length));
|
|
||||||
if (id >= INC) {
|
|
||||||
INC = id + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
150
background/db.js
150
background/db.js
|
@ -1,150 +0,0 @@
|
||||||
/* global addAPI */// common.js
|
|
||||||
/* global chromeLocal */// storage-util.js
|
|
||||||
/* global cloneError */// worker-util.js
|
|
||||||
/* global deepCopy */// toolbox.js
|
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Initialize a database. There are some problems using IndexedDB in Firefox:
|
|
||||||
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
|
|
||||||
Some of them are fixed in FF59:
|
|
||||||
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* exported db */
|
|
||||||
const db = (() => {
|
|
||||||
let exec = async (...args) => (
|
|
||||||
exec = await tryUsingIndexedDB().catch(useChromeStorage)
|
|
||||||
)(...args);
|
|
||||||
const DB = 'stylish';
|
|
||||||
const FALLBACK = 'dbInChromeStorage';
|
|
||||||
const ID_AS_KEY = {[DB]: true};
|
|
||||||
const getStoreName = dbName => dbName === DB ? 'styles' : 'data';
|
|
||||||
const cache = {};
|
|
||||||
const proxies = {};
|
|
||||||
const proxyHandler = {
|
|
||||||
get: ({dbName}, cmd) =>
|
|
||||||
(...args) =>
|
|
||||||
(dbName === DB ? exec : cachedExec)(dbName, cmd, ...args),
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @param {string} dbName
|
|
||||||
* @return {IDBObjectStore | {putMany: function(items:?[]):Promise<?[]>}}
|
|
||||||
*/
|
|
||||||
const getProxy = dbName => proxies[dbName] || (
|
|
||||||
(proxies[dbName] = new Proxy({dbName}, proxyHandler))
|
|
||||||
);
|
|
||||||
addAPI(/** @namespace API */ {
|
|
||||||
drafts: getProxy('drafts'),
|
|
||||||
/** Storage for big items that may exceed 8kB limit of chrome.storage.sync.
|
|
||||||
* To make an item syncable register it with uuidIndex.addCustomId. */
|
|
||||||
prefsDb: getProxy(prefs.STORAGE_KEY),
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
styles: getProxy(DB),
|
|
||||||
};
|
|
||||||
|
|
||||||
async function cachedExec(dbName, cmd, a, b) {
|
|
||||||
const hub = cache[dbName] || (cache[dbName] = {});
|
|
||||||
const res = cmd === 'get' && a in hub ? hub[a] : await exec(...arguments);
|
|
||||||
if (cmd === 'get') {
|
|
||||||
hub[a] = deepCopy(res);
|
|
||||||
} else if (cmd === 'put') {
|
|
||||||
hub[ID_AS_KEY[dbName] ? a.id : b] = deepCopy(a);
|
|
||||||
} else if (cmd === 'delete') {
|
|
||||||
delete hub[a];
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryUsingIndexedDB() {
|
|
||||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
|
||||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
|
||||||
// note that accessing indexedDB may throw, https://github.com/openstyles/stylus/issues/615
|
|
||||||
if (typeof indexedDB === 'undefined') {
|
|
||||||
throw new Error('indexedDB is undefined');
|
|
||||||
}
|
|
||||||
switch (await chromeLocal.getValue(FALLBACK)) {
|
|
||||||
case true: throw null;
|
|
||||||
case false: break;
|
|
||||||
default: await testDB();
|
|
||||||
}
|
|
||||||
chromeLocal.setValue(FALLBACK, false);
|
|
||||||
return dbExecIndexedDB;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testDB() {
|
|
||||||
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
|
|
||||||
await dbExecIndexedDB(DB, 'put', {id});
|
|
||||||
const e = await dbExecIndexedDB(DB, 'get', id);
|
|
||||||
await dbExecIndexedDB(DB, 'delete', e.id); // throws if `e` or id is null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function useChromeStorage(err) {
|
|
||||||
chromeLocal.setValue(FALLBACK, true);
|
|
||||||
if (err) {
|
|
||||||
chromeLocal.setValue(FALLBACK + 'Reason', cloneError(err));
|
|
||||||
console.warn('Failed to access indexedDB. Switched to storage API.', err);
|
|
||||||
}
|
|
||||||
await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
|
|
||||||
const BASES = {};
|
|
||||||
return (dbName, method, ...args) => (
|
|
||||||
BASES[dbName] || (
|
|
||||||
BASES[dbName] = createChromeStorageDB(dbName !== DB && `${dbName}-`)
|
|
||||||
)
|
|
||||||
)[method](...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dbExecIndexedDB(dbName, method, ...args) {
|
|
||||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
|
||||||
const storeName = getStoreName(dbName);
|
|
||||||
const store = (await open(dbName)).transaction([storeName], mode).objectStore(storeName);
|
|
||||||
const fn = method === 'putMany' ? putMany : storeRequest;
|
|
||||||
return fn(store, method, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
function storeRequest(store, method, ...args) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
/** @type {IDBRequest} */
|
|
||||||
const request = store[method](...args);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = reject;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function putMany(store, _method, items) {
|
|
||||||
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function open(name) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(name, 2);
|
|
||||||
request.onsuccess = e => resolve(create(e));
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onupgradeneeded = create;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function create(event) {
|
|
||||||
/** @type IDBDatabase */
|
|
||||||
const idb = event.target.result;
|
|
||||||
const dbName = idb.name;
|
|
||||||
const sn = getStoreName(dbName);
|
|
||||||
if (!idb.objectStoreNames.contains(sn)) {
|
|
||||||
if (event.type === 'success') {
|
|
||||||
idb.close();
|
|
||||||
return new Promise(resolve => {
|
|
||||||
indexedDB.deleteDatabase(dbName).onsuccess = () => {
|
|
||||||
resolve(open(dbName));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
idb.createObjectStore(sn, ID_AS_KEY[dbName] ? {
|
|
||||||
keyPath: 'id',
|
|
||||||
autoIncrement: true,
|
|
||||||
} : undefined);
|
|
||||||
}
|
|
||||||
return idb;
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,232 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
/* global addAPI bgReady */// common.js
|
|
||||||
/* global colorScheme */
|
|
||||||
/* global prefs */
|
|
||||||
/* global tabMan */
|
|
||||||
/* global CHROME FIREFOX UA debounce ignoreChromeError */// toolbox.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* exported iconMan */
|
|
||||||
const iconMan = (() => {
|
|
||||||
const ICON_SIZES = FIREFOX || CHROME && !UA.vivaldi ? [16, 32] : [19, 38];
|
|
||||||
const staleBadges = new Set();
|
|
||||||
const imageDataCache = new Map();
|
|
||||||
const badgeOvr = {color: '', text: ''};
|
|
||||||
// https://github.com/openstyles/stylus/issues/1287 Fenix can't use custom ImageData
|
|
||||||
const FIREFOX_ANDROID = FIREFOX && UA.mobile;
|
|
||||||
let isDark;
|
|
||||||
// https://github.com/openstyles/stylus/issues/335
|
|
||||||
let hasCanvas = FIREFOX_ANDROID ? false : loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
|
|
||||||
.then(({data}) => (hasCanvas = data.some(b => b !== 255)));
|
|
||||||
|
|
||||||
addAPI(/** @namespace API */ {
|
|
||||||
/**
|
|
||||||
* @param {(number|string)[]} styleIds
|
|
||||||
* @param {boolean} [lazyBadge=false] preventing flicker during page load
|
|
||||||
*/
|
|
||||||
updateIconBadge(styleIds, {lazyBadge} = {}) {
|
|
||||||
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
|
|
||||||
const {frameId, tab: {id: tabId}} = this.sender;
|
|
||||||
const value = styleIds.length ? styleIds.map(Number) : undefined;
|
|
||||||
tabMan.set(tabId, 'styleIds', frameId, value);
|
|
||||||
debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
|
|
||||||
staleBadges.add(tabId);
|
|
||||||
if (!frameId) refreshIcon(tabId, true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
|
|
||||||
if (!frameId) tabMan.set(tabId, 'styleIds', undefined);
|
|
||||||
});
|
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
|
||||||
if (port.name === 'iframe') {
|
|
||||||
port.onDisconnect.addListener(onPortDisconnected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
colorScheme.onChange(val => {
|
|
||||||
isDark = val;
|
|
||||||
if (prefs.get('iconset') === -1) {
|
|
||||||
debounce(refreshAllIcons);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bgReady.all.then(() => {
|
|
||||||
prefs.subscribe([
|
|
||||||
'disableAll',
|
|
||||||
'badgeDisabled',
|
|
||||||
'badgeNormal',
|
|
||||||
], () => debounce(refreshIconBadgeColor), {runNow: true});
|
|
||||||
prefs.subscribe([
|
|
||||||
'show-badge',
|
|
||||||
], () => debounce(refreshAllIconsBadgeText), {runNow: true});
|
|
||||||
prefs.subscribe([
|
|
||||||
'disableAll',
|
|
||||||
'iconset',
|
|
||||||
], () => debounce(refreshAllIcons), {runNow: true});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
/** Calling with no params clears the override */
|
|
||||||
overrideBadge({text = '', color = '', title = ''} = {}) {
|
|
||||||
if (badgeOvr.text === text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
badgeOvr.text = text;
|
|
||||||
badgeOvr.color = color;
|
|
||||||
refreshIconBadgeColor();
|
|
||||||
setBadgeText({text});
|
|
||||||
for (const tabId of tabMan.list()) {
|
|
||||||
if (text) {
|
|
||||||
setBadgeText({tabId, text});
|
|
||||||
} else {
|
|
||||||
refreshIconBadgeText(tabId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chrome.browserAction.setTitle({
|
|
||||||
title: title && chrome.i18n.getMessage(title) || title || '',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function onPortDisconnected({sender}) {
|
|
||||||
if (tabMan.get(sender.tab.id, 'styleIds')) {
|
|
||||||
API.updateIconBadge.call({sender}, [], {lazyBadge: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshIconBadgeText(tabId) {
|
|
||||||
if (badgeOvr.text) return;
|
|
||||||
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
|
|
||||||
setBadgeText({tabId, text});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconName(hasStyles = false) {
|
|
||||||
const i = prefs.get('iconset');
|
|
||||||
const prefix = i === 0 || i === -1 && isDark ? '' : 'light/';
|
|
||||||
const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
|
|
||||||
return `${prefix}$SIZE$${postfix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshIcon(tabId, force = false) {
|
|
||||||
const oldIcon = tabMan.get(tabId, 'icon');
|
|
||||||
const newIcon = getIconName(tabMan.get(tabId, 'styleIds', 0));
|
|
||||||
// (changing the icon only for the main page, frameId = 0)
|
|
||||||
|
|
||||||
if (!force && oldIcon === newIcon) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tabMan.set(tabId, 'icon', newIcon);
|
|
||||||
setIcon({
|
|
||||||
path: getIconPath(newIcon),
|
|
||||||
tabId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconPath(icon) {
|
|
||||||
return ICON_SIZES.reduce(
|
|
||||||
(obj, size) => {
|
|
||||||
obj[size] = `/images/icon/${icon.replace('$SIZE$', size)}.png`;
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return {number | ''} */
|
|
||||||
function getStyleCount(tabId) {
|
|
||||||
const allIds = new Set();
|
|
||||||
const data = tabMan.get(tabId, 'styleIds') || {};
|
|
||||||
Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
|
|
||||||
return allIds.size || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caches imageData for icon paths
|
|
||||||
async function loadImage(url) {
|
|
||||||
const {OffscreenCanvas} = !FIREFOX && self.createImageBitmap && self || {};
|
|
||||||
const img = OffscreenCanvas
|
|
||||||
? await createImageBitmap(await (await fetch(url)).blob())
|
|
||||||
: await new Promise((resolve, reject) =>
|
|
||||||
Object.assign(new Image(), {
|
|
||||||
src: url,
|
|
||||||
onload: e => resolve(e.target),
|
|
||||||
onerror: reject,
|
|
||||||
}));
|
|
||||||
const {width: w, height: h} = img;
|
|
||||||
const canvas = OffscreenCanvas
|
|
||||||
? new OffscreenCanvas(w, h)
|
|
||||||
: Object.assign(document.createElement('canvas'), {width: w, height: h});
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.drawImage(img, 0, 0, w, h);
|
|
||||||
const result = ctx.getImageData(0, 0, w, h);
|
|
||||||
imageDataCache.set(url, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshGlobalIcon() {
|
|
||||||
setIcon({
|
|
||||||
path: getIconPath(getIconName()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshIconBadgeColor() {
|
|
||||||
setBadgeBackgroundColor({
|
|
||||||
color: badgeOvr.color ||
|
|
||||||
prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshAllIcons() {
|
|
||||||
for (const tabId of tabMan.list()) {
|
|
||||||
refreshIcon(tabId);
|
|
||||||
}
|
|
||||||
refreshGlobalIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshAllIconsBadgeText() {
|
|
||||||
for (const tabId of tabMan.list()) {
|
|
||||||
refreshIconBadgeText(tabId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshStaleBadges() {
|
|
||||||
for (const tabId of staleBadges) {
|
|
||||||
refreshIconBadgeText(tabId);
|
|
||||||
}
|
|
||||||
staleBadges.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeCall(method, data) {
|
|
||||||
const {browserAction = {}} = chrome;
|
|
||||||
const fn = browserAction[method];
|
|
||||||
if (fn) {
|
|
||||||
try {
|
|
||||||
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
|
||||||
fn.call(browserAction, data, ignoreChromeError);
|
|
||||||
} catch (e) {
|
|
||||||
// FIXME: skip pre-rendered tabs?
|
|
||||||
fn.call(browserAction, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.browserAction.TabIconDetails} data */
|
|
||||||
async function setIcon(data) {
|
|
||||||
if (hasCanvas === true || await hasCanvas) {
|
|
||||||
data.imageData = {};
|
|
||||||
for (const [key, url] of Object.entries(data.path)) {
|
|
||||||
data.imageData[key] = imageDataCache.get(url) || await loadImage(url);
|
|
||||||
}
|
|
||||||
delete data.path;
|
|
||||||
}
|
|
||||||
safeCall('setIcon', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.browserAction.BadgeTextDetails} data */
|
|
||||||
function setBadgeText(data) {
|
|
||||||
safeCall('setBadgeText', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.browserAction.BadgeBackgroundColorDetails} data */
|
|
||||||
function setBadgeBackgroundColor(data) {
|
|
||||||
safeCall('setBadgeBackgroundColor', data);
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,103 +0,0 @@
|
||||||
/* global CHROME FIREFOX URLS deepEqual ignoreChromeError */// toolbox.js
|
|
||||||
/* global bgReady */// common.js
|
|
||||||
/* global msg */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* exported navMan */
|
|
||||||
const navMan = (() => {
|
|
||||||
const listeners = new Set();
|
|
||||||
let prevData = {};
|
|
||||||
|
|
||||||
chrome.webNavigation.onCommitted.addListener(onNavigation.bind('committed'));
|
|
||||||
chrome.webNavigation.onHistoryStateUpdated.addListener(onFakeNavigation.bind('history'));
|
|
||||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(onFakeNavigation.bind('hash'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
/** @param {function(data: Object, type: ('committed'|'history'|'hash'))} fn */
|
|
||||||
onUrlChange(fn) {
|
|
||||||
listeners.add(fn);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @this {string} type */
|
|
||||||
async function onNavigation(data) {
|
|
||||||
if (CHROME && data.timeStamp === prevData.timeStamp && deepEqual(data, prevData)) {
|
|
||||||
return; // Chrome bug: listener is called twice with identical data
|
|
||||||
}
|
|
||||||
prevData = data;
|
|
||||||
if (CHROME &&
|
|
||||||
URLS.chromeProtectsNTP &&
|
|
||||||
data.url.startsWith('https://www.google.') &&
|
|
||||||
data.url.includes('/_/chrome/newtab?')) {
|
|
||||||
// Modern Chrome switched to WebUI NTP so this is obsolete, but there may be exceptions
|
|
||||||
// TODO: investigate, and maybe use a separate listener for CHROME <= ver
|
|
||||||
const tab = await browser.tabs.get(data.tabId);
|
|
||||||
const url = tab.pendingUrl || tab.url;
|
|
||||||
if (url === 'chrome://newtab/') {
|
|
||||||
data.url = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listeners.forEach(fn => fn(data, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @this {string} type */
|
|
||||||
function onFakeNavigation(data) {
|
|
||||||
const {url, frameId} = data;
|
|
||||||
onNavigation.call(this, data);
|
|
||||||
msg.sendTab(data.tabId, {method: 'urlChanged', url}, {frameId})
|
|
||||||
.catch(msg.ignoreError);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
bgReady.all.then(() => {
|
|
||||||
/*
|
|
||||||
* Expose style version on greasyfork/sleazyfork 1) info page and 2) code page
|
|
||||||
* Not using manifest.json as adding a content script disables the extension on update.
|
|
||||||
*/
|
|
||||||
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
|
|
||||||
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
|
|
||||||
chrome.tabs.executeScript(tabId, {
|
|
||||||
file: '/content/install-hook-greasyfork.js',
|
|
||||||
runAt: 'document_start',
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
url: [
|
|
||||||
{hostEquals: 'greasyfork.org', urlMatches},
|
|
||||||
{hostEquals: 'sleazyfork.org', urlMatches},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Removes the Get Stylus button on style pages.
|
|
||||||
* Not using manifest.json as adding a content script disables the extension on update.
|
|
||||||
*/
|
|
||||||
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
|
|
||||||
chrome.tabs.executeScript(tabId, {
|
|
||||||
file: '/content/install-hook-userstylesworld.js',
|
|
||||||
runAt: 'document_start',
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
url: [
|
|
||||||
{hostEquals: 'userstyles.world'},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
* FF misses some about:blank iframes so we inject our content script explicitly
|
|
||||||
*/
|
|
||||||
if (FIREFOX) {
|
|
||||||
chrome.webNavigation.onDOMContentLoaded.addListener(async ({tabId, frameId}) => {
|
|
||||||
if (frameId &&
|
|
||||||
!await msg.sendTab(tabId, {method: 'ping'}, {frameId}).catch(ignoreChromeError)) {
|
|
||||||
for (const file of chrome.runtime.getManifest().content_scripts[0].js) {
|
|
||||||
chrome.tabs.executeScript(tabId, {
|
|
||||||
frameId,
|
|
||||||
file,
|
|
||||||
matchAboutBlank: true,
|
|
||||||
}, ignoreChromeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
url: [{urlEquals: 'about:blank'}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
811
background/storage.js
Normal file
811
background/storage.js
Normal file
|
@ -0,0 +1,811 @@
|
||||||
|
/* global LZString */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
||||||
|
/(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
|
||||||
|
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
|
||||||
|
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var SLOPPY_REGEXP_PREFIX = '\0';
|
||||||
|
|
||||||
|
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||||
|
// the browsers, especially Firefox, may apply all transitions on page load
|
||||||
|
const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }';
|
||||||
|
const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/;
|
||||||
|
|
||||||
|
// Note, only 'var'-declared variables are visible from another extension page
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var cachedStyles = {
|
||||||
|
list: null, // array of all styles
|
||||||
|
byId: new Map(), // all styles indexed by id
|
||||||
|
filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
|
||||||
|
regexps: new Map(), // compiled style regexps
|
||||||
|
urlDomains: new Map(), // getDomain() results for 100 last checked urls
|
||||||
|
needTransitionPatch: new Map(), // FF bug workaround
|
||||||
|
mutex: {
|
||||||
|
inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls
|
||||||
|
// (initially 'true' to prevent rogue getStyles before dbExec.initialized)
|
||||||
|
onDone: [], // to getStyles() are queued and resolved when the first one finishes
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var chromeLocal = {
|
||||||
|
get(options) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.storage.local.get(options, data => resolve(data));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
set(data) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.storage.local.set(data, () => resolve(data));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove(keyOrKeys) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.storage.local.remove(keyOrKeys, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getValue(key) {
|
||||||
|
return chromeLocal.get(key).then(data => data[key]);
|
||||||
|
},
|
||||||
|
setValue(key, value) {
|
||||||
|
return chromeLocal.set({[key]: value});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var chromeSync = {
|
||||||
|
get(options) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.storage.sync.get(options, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
set(data) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.storage.sync.set(data, () => resolve(data));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getLZValue(key) {
|
||||||
|
return chromeSync.getLZValues([key]).then(data => data[key]);
|
||||||
|
},
|
||||||
|
getLZValues(keys) {
|
||||||
|
return chromeSync.get(keys).then((data = {}) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = data[key];
|
||||||
|
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setLZValue(key, value) {
|
||||||
|
return chromeSync.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var dbExec = dbExecIndexedDB;
|
||||||
|
dbExec.initialized = false;
|
||||||
|
|
||||||
|
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||||
|
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||||
|
// for reliablility and in localStorage for fast synchronous access
|
||||||
|
// (FF may block localStorage depending on its privacy options)
|
||||||
|
do {
|
||||||
|
const done = () => {
|
||||||
|
cachedStyles.mutex.inProgress = false;
|
||||||
|
getStyles().then(() => {
|
||||||
|
dbExec.initialized = true;
|
||||||
|
window.dispatchEvent(new Event('storageReady'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const fallback = () => {
|
||||||
|
dbExec = dbExecChromeStorage;
|
||||||
|
chromeLocal.set({dbInChromeStorage: true});
|
||||||
|
localStorage.dbInChromeStorage = 'true';
|
||||||
|
ignoreChromeError();
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
const fallbackSet = localStorage.dbInChromeStorage;
|
||||||
|
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
||||||
|
fallback();
|
||||||
|
break;
|
||||||
|
} else if (fallbackSet === 'false') {
|
||||||
|
done();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chromeLocal.get('dbInChromeStorage')
|
||||||
|
.then(data =>
|
||||||
|
data && data.dbInChromeStorage && Promise.reject())
|
||||||
|
.then(() =>
|
||||||
|
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
||||||
|
Promise.reject())
|
||||||
|
.then(({target}) => (
|
||||||
|
(target.result || [])[0] ?
|
||||||
|
Promise.reject('ok') :
|
||||||
|
dbExecIndexedDB('put', {id: -1})))
|
||||||
|
.then(() =>
|
||||||
|
dbExecIndexedDB('get', -1))
|
||||||
|
.then(({target}) => (
|
||||||
|
(target.result || {}).id === -1 ?
|
||||||
|
dbExecIndexedDB('delete', -1) :
|
||||||
|
Promise.reject()))
|
||||||
|
.then(() =>
|
||||||
|
Promise.reject('ok'))
|
||||||
|
.catch(result => {
|
||||||
|
if (result === 'ok') {
|
||||||
|
chromeLocal.set({dbInChromeStorage: false});
|
||||||
|
localStorage.dbInChromeStorage = 'false';
|
||||||
|
done();
|
||||||
|
} else {
|
||||||
|
fallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} while (0);
|
||||||
|
|
||||||
|
|
||||||
|
function dbExecIndexedDB(method, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Object.assign(indexedDB.open('stylish', 2), {
|
||||||
|
onsuccess(event) {
|
||||||
|
const database = event.target.result;
|
||||||
|
if (!method) {
|
||||||
|
resolve(database);
|
||||||
|
} else {
|
||||||
|
const transaction = database.transaction(['styles'], 'readwrite');
|
||||||
|
const store = transaction.objectStore('styles');
|
||||||
|
Object.assign(store[method](...args), {
|
||||||
|
onsuccess: event => resolve(event, store, transaction, database),
|
||||||
|
onerror: reject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror(event) {
|
||||||
|
console.warn(event.target.error || event.target.errorCode);
|
||||||
|
reject(event);
|
||||||
|
},
|
||||||
|
onupgradeneeded(event) {
|
||||||
|
if (event.oldVersion === 0) {
|
||||||
|
event.target.result.createObjectStore('styles', {
|
||||||
|
keyPath: 'id',
|
||||||
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dbExecChromeStorage(method, data) {
|
||||||
|
const STYLE_KEY_PREFIX = 'style-';
|
||||||
|
switch (method) {
|
||||||
|
case 'get':
|
||||||
|
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||||
|
.then(result => ({target: {result}}));
|
||||||
|
|
||||||
|
case 'put':
|
||||||
|
if (!data.id) {
|
||||||
|
return getStyles().then(() => {
|
||||||
|
data.id = 1;
|
||||||
|
for (const style of cachedStyles.list) {
|
||||||
|
data.id = Math.max(data.id, style.id + 1);
|
||||||
|
}
|
||||||
|
return dbExecChromeStorage('put', data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||||
|
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||||
|
|
||||||
|
case 'getAll':
|
||||||
|
return chromeLocal.get(null).then(storage => {
|
||||||
|
const styles = [];
|
||||||
|
const leftovers = [];
|
||||||
|
for (const key in storage) {
|
||||||
|
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||||
|
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||||
|
styles.push(storage[key]);
|
||||||
|
} else if (key.startsWith('tempUsercssCode')) {
|
||||||
|
leftovers.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (leftovers.length) {
|
||||||
|
chromeLocal.remove(leftovers);
|
||||||
|
}
|
||||||
|
return {target: {result: styles}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getStyles(options) {
|
||||||
|
if (cachedStyles.list) {
|
||||||
|
return Promise.resolve(filterStyles(options));
|
||||||
|
}
|
||||||
|
if (cachedStyles.mutex.inProgress) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
cachedStyles.mutex.onDone.push({options, resolve});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cachedStyles.mutex.inProgress = true;
|
||||||
|
|
||||||
|
return dbExec('getAll').then(event => {
|
||||||
|
cachedStyles.list = event.target.result || [];
|
||||||
|
cachedStyles.byId.clear();
|
||||||
|
for (const style of cachedStyles.list) {
|
||||||
|
cachedStyles.byId.set(style.id, style);
|
||||||
|
if (!style.name) {
|
||||||
|
style.name = 'ID: ' + style.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedStyles.mutex.inProgress = false;
|
||||||
|
for (const {options, resolve} of cachedStyles.mutex.onDone) {
|
||||||
|
resolve(filterStyles(options));
|
||||||
|
}
|
||||||
|
cachedStyles.mutex.onDone = [];
|
||||||
|
return filterStyles(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function filterStyles({
|
||||||
|
enabled = null,
|
||||||
|
id = null,
|
||||||
|
matchUrl = null,
|
||||||
|
md5Url = null,
|
||||||
|
asHash = null,
|
||||||
|
strictRegexp = true, // used by the popup to detect bad regexps
|
||||||
|
} = {}) {
|
||||||
|
enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
|
||||||
|
typeof enabled === 'string' ? enabled === 'true' : null;
|
||||||
|
id = id === null ? null : Number(id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
enabled === null &&
|
||||||
|
id === null &&
|
||||||
|
matchUrl === null &&
|
||||||
|
md5Url === null &&
|
||||||
|
asHash !== true
|
||||||
|
) {
|
||||||
|
return cachedStyles.list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchUrl && !URLS.supported(matchUrl)) {
|
||||||
|
return asHash ? {} : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const blankHash = asHash && {
|
||||||
|
disableAll: prefs.get('disableAll'),
|
||||||
|
exposeIframes: prefs.get('exposeIframes'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheKey = [enabled, id, matchUrl, md5Url, asHash, strictRegexp].join('\t');
|
||||||
|
const cached = cachedStyles.filters.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
cached.hits++;
|
||||||
|
cached.lastHit = Date.now();
|
||||||
|
return asHash
|
||||||
|
? Object.assign(blankHash, cached.styles)
|
||||||
|
: cached.styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterStylesInternal({
|
||||||
|
enabled,
|
||||||
|
id,
|
||||||
|
matchUrl,
|
||||||
|
md5Url,
|
||||||
|
asHash,
|
||||||
|
strictRegexp,
|
||||||
|
blankHash,
|
||||||
|
cacheKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function filterStylesInternal({
|
||||||
|
// js engines don't like big functions (V8 often deoptimized the original filterStyles)
|
||||||
|
// it also makes sense to extract the less frequently executed code
|
||||||
|
enabled,
|
||||||
|
id,
|
||||||
|
matchUrl,
|
||||||
|
md5Url,
|
||||||
|
asHash,
|
||||||
|
strictRegexp,
|
||||||
|
blankHash,
|
||||||
|
cacheKey,
|
||||||
|
}) {
|
||||||
|
if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
|
||||||
|
cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
|
||||||
|
for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) {
|
||||||
|
const firstKey = cachedStyles.urlDomains.keys().next().value;
|
||||||
|
cachedStyles.urlDomains.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = id === null
|
||||||
|
? cachedStyles.list
|
||||||
|
: [cachedStyles.byId.get(id)];
|
||||||
|
if (!styles[0]) {
|
||||||
|
// may happen when users [accidentally] reopen an old URL
|
||||||
|
// of edit.html with a non-existent style id parameter
|
||||||
|
return asHash ? blankHash : [];
|
||||||
|
}
|
||||||
|
const filtered = asHash ? {} : [];
|
||||||
|
const needSections = asHash || matchUrl !== null;
|
||||||
|
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
||||||
|
|
||||||
|
let style;
|
||||||
|
for (let i = 0; (style = styles[i]); i++) {
|
||||||
|
if ((enabled === null || style.enabled === enabled)
|
||||||
|
&& (md5Url === null || style.md5Url === md5Url)
|
||||||
|
&& (id === null || style.id === id)) {
|
||||||
|
const sections = needSections &&
|
||||||
|
getApplicableSections({
|
||||||
|
style,
|
||||||
|
matchUrl,
|
||||||
|
strictRegexp,
|
||||||
|
stopOnFirst: !asHash,
|
||||||
|
skipUrlCheck: true,
|
||||||
|
matchUrlBase,
|
||||||
|
});
|
||||||
|
if (asHash) {
|
||||||
|
if (sections.length) {
|
||||||
|
filtered[style.id] = sections;
|
||||||
|
}
|
||||||
|
} else if (matchUrl === null || sections.length) {
|
||||||
|
filtered.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedStyles.filters.set(cacheKey, {
|
||||||
|
styles: filtered,
|
||||||
|
lastHit: Date.now(),
|
||||||
|
hits: 1,
|
||||||
|
});
|
||||||
|
if (cachedStyles.filters.size > 10000) {
|
||||||
|
cleanupCachedFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// a shallow copy is needed because the cache doesn't store options like disableAll
|
||||||
|
return asHash
|
||||||
|
? Object.assign(blankHash, filtered)
|
||||||
|
: filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function saveStyle(style) {
|
||||||
|
const id = Number(style.id) || null;
|
||||||
|
const reason = style.reason;
|
||||||
|
const notify = style.notify !== false;
|
||||||
|
delete style.method;
|
||||||
|
delete style.reason;
|
||||||
|
delete style.notify;
|
||||||
|
if (!style.name) {
|
||||||
|
delete style.name;
|
||||||
|
}
|
||||||
|
let existed;
|
||||||
|
let codeIsUpdated;
|
||||||
|
|
||||||
|
return maybeCalcDigest()
|
||||||
|
.then(maybeImportFix)
|
||||||
|
.then(decide);
|
||||||
|
|
||||||
|
function maybeCalcDigest() {
|
||||||
|
if (['install', 'update', 'update-digest'].includes(reason)) {
|
||||||
|
return calcStyleDigest(style).then(digest => {
|
||||||
|
style.originalDigest = digest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeImportFix() {
|
||||||
|
if (reason === 'import') {
|
||||||
|
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
||||||
|
delete style.styleDigest; // TODO: remove in the future
|
||||||
|
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
||||||
|
delete style.originalDigest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decide() {
|
||||||
|
if (id !== null) {
|
||||||
|
// Update or create
|
||||||
|
style.id = id;
|
||||||
|
return dbExec('get', id).then((event, store) => {
|
||||||
|
const oldStyle = event.target.result;
|
||||||
|
existed = Boolean(oldStyle);
|
||||||
|
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||||
|
style = Object.assign({installDate: Date.now()}, oldStyle, style);
|
||||||
|
return write(style, store);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create
|
||||||
|
delete style.id;
|
||||||
|
style = Object.assign({
|
||||||
|
// Set optional things if they're undefined
|
||||||
|
enabled: true,
|
||||||
|
updateUrl: null,
|
||||||
|
md5Url: null,
|
||||||
|
url: null,
|
||||||
|
originalMd5: null,
|
||||||
|
}, style);
|
||||||
|
return write(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(style, store) {
|
||||||
|
style.sections = normalizeStyleSections(style);
|
||||||
|
if (store) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
store.put(style).onsuccess = event => resolve(done(event));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return dbExec('put', style).then(done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(event) {
|
||||||
|
if (reason === 'update-digest') {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
style.id = style.id || event.target.result;
|
||||||
|
invalidateCache(existed ? {updated: style} : {added: style});
|
||||||
|
if (notify) {
|
||||||
|
notifyAllTabs({
|
||||||
|
method: existed ? 'styleUpdated' : 'styleAdded',
|
||||||
|
style, codeIsUpdated, reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deleteStyle({id, notify = true}) {
|
||||||
|
id = Number(id);
|
||||||
|
return dbExec('delete', id).then(() => {
|
||||||
|
invalidateCache({deletedId: id});
|
||||||
|
if (notify) {
|
||||||
|
notifyAllTabs({method: 'styleDeleted', id});
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getApplicableSections({
|
||||||
|
style,
|
||||||
|
matchUrl,
|
||||||
|
strictRegexp = true,
|
||||||
|
// filterStylesInternal() sets the following to avoid recalc on each style:
|
||||||
|
stopOnFirst,
|
||||||
|
skipUrlCheck,
|
||||||
|
matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
|
||||||
|
// as per spec the fragment portion is ignored in @-moz-document:
|
||||||
|
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
||||||
|
// but the spec is outdated and doesn't account for SPA sites
|
||||||
|
// so we only respect it in case of url("http://exact.url/without/hash")
|
||||||
|
}) {
|
||||||
|
if (!skipUrlCheck && !URLS.supported(matchUrl)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const sections = [];
|
||||||
|
for (const section of style.sections) {
|
||||||
|
const {urls, domains, urlPrefixes, regexps, code} = section;
|
||||||
|
const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length;
|
||||||
|
const isMatching = !isGlobal && (
|
||||||
|
urls.length
|
||||||
|
&& (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase))
|
||||||
|
|| urlPrefixes.length
|
||||||
|
&& arraySomeIsPrefix(urlPrefixes, matchUrl)
|
||||||
|
|| domains.length
|
||||||
|
&& arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|
||||||
|
|| regexps.length
|
||||||
|
&& arraySomeMatches(regexps, matchUrl, strictRegexp));
|
||||||
|
if (isGlobal && !styleCodeEmpty(code) || isMatching) {
|
||||||
|
sections.push(section);
|
||||||
|
if (stopOnFirst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
|
||||||
|
function arraySomeIsPrefix(array, string) {
|
||||||
|
for (const prefix of array) {
|
||||||
|
if (string.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arraySomeIn(array, haystack) {
|
||||||
|
for (const el of array) {
|
||||||
|
if (haystack.indexOf(el) >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arraySomeMatches(array, matchUrl, strictRegexp) {
|
||||||
|
for (const regexp of array) {
|
||||||
|
for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
|
||||||
|
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
||||||
|
let rx = cachedStyles.regexps.get(cacheKey);
|
||||||
|
if (rx === false) {
|
||||||
|
// invalid regexp
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!rx) {
|
||||||
|
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||||
|
rx = tryRegExp(anchored);
|
||||||
|
cachedStyles.regexps.set(cacheKey, rx || false);
|
||||||
|
if (!rx) {
|
||||||
|
// invalid regexp
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rx.test(matchUrl)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function styleCodeEmpty(code) {
|
||||||
|
// Collect the global section if it's not empty, not comment-only, not namespace-only.
|
||||||
|
const cmtOpen = code && code.indexOf('/*');
|
||||||
|
if (cmtOpen >= 0) {
|
||||||
|
const cmtCloseLast = code.lastIndexOf('*/');
|
||||||
|
if (cmtCloseLast < 0) {
|
||||||
|
code = code.substr(0, cmtOpen);
|
||||||
|
} else {
|
||||||
|
code = code.substr(0, cmtOpen) +
|
||||||
|
code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
|
||||||
|
code.substr(cmtCloseLast + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !code
|
||||||
|
|| !code.trim()
|
||||||
|
|| code.includes('@namespace') && !code.replace(RX_NAMESPACE, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
|
if (!a || !b) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (a.length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// order of sections should be identical to account for the case of multiple
|
||||||
|
// sections matching the same URL because the order of rules is part of cascading
|
||||||
|
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
||||||
|
|
||||||
|
function propertiesEqual(secA, secB) {
|
||||||
|
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||||
|
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalOrEmpty(a, b, telltale, comparator) {
|
||||||
|
const typeA = a && typeof a[telltale] === 'function';
|
||||||
|
const typeB = b && typeof b[telltale] === 'function';
|
||||||
|
return (
|
||||||
|
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||||
|
(b === null || b === undefined || (typeB && !b.length))
|
||||||
|
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayMirrors(array1, array2) {
|
||||||
|
return (
|
||||||
|
array1.every(el => array2.includes(el)) &&
|
||||||
|
array2.every(el => array1.includes(el))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function invalidateCache({added, updated, deletedId} = {}) {
|
||||||
|
if (!cachedStyles.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = added ? added.id : updated ? updated.id : deletedId;
|
||||||
|
const cached = cachedStyles.byId.get(id);
|
||||||
|
if (updated) {
|
||||||
|
if (cached) {
|
||||||
|
Object.assign(cached, updated);
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
cachedStyles.needTransitionPatch.delete(id);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
added = updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (added) {
|
||||||
|
if (!cached) {
|
||||||
|
cachedStyles.list.push(added);
|
||||||
|
cachedStyles.byId.set(added.id, added);
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
cachedStyles.needTransitionPatch.delete(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deletedId !== undefined) {
|
||||||
|
if (cached) {
|
||||||
|
const cachedIndex = cachedStyles.list.indexOf(cached);
|
||||||
|
cachedStyles.list.splice(cachedIndex, 1);
|
||||||
|
cachedStyles.byId.delete(deletedId);
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
cachedStyles.needTransitionPatch.delete(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cachedStyles.list = null;
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
cachedStyles.needTransitionPatch.clear(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function cleanupCachedFilters({force = false} = {}) {
|
||||||
|
if (!force) {
|
||||||
|
debounce(cleanupCachedFilters, 1000, {force: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const size = cachedStyles.filters.size;
|
||||||
|
const oldestHit = cachedStyles.filters.values().next().value.lastHit;
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSpan = now - oldestHit;
|
||||||
|
const recencyWeight = 5 / size;
|
||||||
|
const hitWeight = 1 / 4; // we make ~4 hits per URL
|
||||||
|
const lastHitWeight = 10;
|
||||||
|
// delete the oldest 10%
|
||||||
|
[...cachedStyles.filters.entries()]
|
||||||
|
.map(([id, v], index) => ({
|
||||||
|
id,
|
||||||
|
weight:
|
||||||
|
index * recencyWeight +
|
||||||
|
v.hits * hitWeight +
|
||||||
|
(v.lastHit - oldestHit) / timeSpan * lastHitWeight,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.weight - b.weight)
|
||||||
|
.slice(0, size / 10 + 1)
|
||||||
|
.forEach(({id}) => cachedStyles.filters.delete(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getDomains(url) {
|
||||||
|
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
|
||||||
|
if (!d || url.startsWith('file:')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const domains = [d];
|
||||||
|
while (d.indexOf('.') !== -1) {
|
||||||
|
d = d.substring(d.indexOf('.') + 1);
|
||||||
|
domains.push(d);
|
||||||
|
}
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function normalizeStyleSections({sections}) {
|
||||||
|
// retain known properties in an arbitrarily predefined order
|
||||||
|
return (sections || []).map(section => ({
|
||||||
|
code: section.code || '',
|
||||||
|
urls: section.urls || [],
|
||||||
|
urlPrefixes: section.urlPrefixes || [],
|
||||||
|
domains: section.domains || [],
|
||||||
|
regexps: section.regexps || [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function calcStyleDigest(style) {
|
||||||
|
const jsonString = style.usercssData ?
|
||||||
|
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
||||||
|
const text = new TextEncoder('utf-8').encode(jsonString);
|
||||||
|
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||||
|
|
||||||
|
function hex(buffer) {
|
||||||
|
const parts = [];
|
||||||
|
const PAD8 = '00000000';
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
for (let i = 0; i < view.byteLength; i += 4) {
|
||||||
|
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
||||||
|
}
|
||||||
|
return parts.join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleCssTransitionBug({tabId, frameId, url, styles}) {
|
||||||
|
for (let id in styles) {
|
||||||
|
id |= 0;
|
||||||
|
if (!id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let need = cachedStyles.needTransitionPatch.get(id);
|
||||||
|
if (need === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (need !== true) {
|
||||||
|
need = styles[id].some(sectionContainsTransitions);
|
||||||
|
cachedStyles.needTransitionPatch.set(id, need);
|
||||||
|
if (!need) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (FIREFOX && !url.startsWith(URLS.ownOrigin)) {
|
||||||
|
patchFirefox();
|
||||||
|
} else {
|
||||||
|
styles.needTransitionPatch = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchFirefox() {
|
||||||
|
const options = {
|
||||||
|
frameId,
|
||||||
|
code: CSS_TRANSITION_SUPPRESSOR,
|
||||||
|
matchAboutBlank: true,
|
||||||
|
};
|
||||||
|
if (FIREFOX >= 53) {
|
||||||
|
options.cssOrigin = 'user';
|
||||||
|
}
|
||||||
|
browser.tabs.insertCSS(tabId, Object.assign(options, {
|
||||||
|
runAt: 'document_start',
|
||||||
|
})).then(() => setTimeout(() => {
|
||||||
|
browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError);
|
||||||
|
})).catch(ignoreChromeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionContainsTransitions(section) {
|
||||||
|
let code = section.code;
|
||||||
|
const firstTransition = code.indexOf('transition');
|
||||||
|
if (firstTransition < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstCmt = code.indexOf('/*');
|
||||||
|
// check the part before the first comment
|
||||||
|
if (firstCmt < 0 || firstTransition < firstCmt) {
|
||||||
|
if (quickCheckAround(code, firstTransition)) {
|
||||||
|
return true;
|
||||||
|
} else if (firstCmt < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check the rest
|
||||||
|
const lastCmt = code.lastIndexOf('*/');
|
||||||
|
if (lastCmt < firstCmt) {
|
||||||
|
// the comment is unclosed and we already checked the preceding part
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mid = code.slice(firstCmt, lastCmt + 2);
|
||||||
|
mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, '');
|
||||||
|
code = mid + code.slice(lastCmt + 2);
|
||||||
|
return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickCheckAround(code, pos = code.indexOf('transition')) {
|
||||||
|
return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,791 +0,0 @@
|
||||||
/* global API msg */// msg.js
|
|
||||||
/* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
|
|
||||||
/* global bgReady createCache uuidIndex */// common.js
|
|
||||||
/* global calcStyleDigest styleCodeEmpty */// sections-util.js
|
|
||||||
/* global db */
|
|
||||||
/* global prefs */
|
|
||||||
/* global tabMan */
|
|
||||||
/* global usercssMan */
|
|
||||||
/* global colorScheme */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
This style manager is a layer between content script and the DB. When a style
|
|
||||||
is added/updated, it broadcast a message to content script and the content
|
|
||||||
script would try to fetch the new code.
|
|
||||||
|
|
||||||
The live preview feature relies on `runtime.connect` and `port.onDisconnect`
|
|
||||||
to cleanup the temporary code. See livePreview in /edit.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const styleUtil = {};
|
|
||||||
|
|
||||||
/* exported styleMan */
|
|
||||||
const styleMan = (() => {
|
|
||||||
|
|
||||||
Object.assign(styleUtil, {
|
|
||||||
id2style,
|
|
||||||
handleSave,
|
|
||||||
uuid2style,
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region Declarations
|
|
||||||
|
|
||||||
/** @typedef {{
|
|
||||||
style: StyleObj,
|
|
||||||
preview?: StyleObj,
|
|
||||||
appliesTo: Set<string>,
|
|
||||||
}} StyleMapData */
|
|
||||||
/** @type {Map<number,StyleMapData>} */
|
|
||||||
const dataMap = new Map();
|
|
||||||
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
|
|
||||||
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
|
|
||||||
const cachedStyleForUrl = createCache({
|
|
||||||
onDeleted(url, cache) {
|
|
||||||
for (const section of Object.values(cache.sections)) {
|
|
||||||
const data = id2data(section.id);
|
|
||||||
if (data) data.appliesTo.delete(url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const BAD_MATCHER = {test: () => false};
|
|
||||||
const compileRe = createCompiler(text => `^(${text})$`);
|
|
||||||
const compileSloppyRe = createCompiler(text => `^${text}$`);
|
|
||||||
const compileExclusion = createCompiler(buildExclusion);
|
|
||||||
const uuidv4 = crypto.randomUUID ? crypto.randomUUID.bind(crypto) : (() => {
|
|
||||||
const seeds = crypto.getRandomValues(new Uint16Array(8));
|
|
||||||
// 00001111-2222-M333-N444-555566667777
|
|
||||||
seeds[3] = seeds[3] & 0x0FFF | 0x4000; // UUID version 4, M = 4
|
|
||||||
seeds[4] = seeds[4] & 0x3FFF | 0x8000; // UUID variant 1, N = 8..0xB
|
|
||||||
return Array.from(seeds, hex4dashed).join('');
|
|
||||||
});
|
|
||||||
const MISSING_PROPS = {
|
|
||||||
name: style => `ID: ${style.id}`,
|
|
||||||
_id: () => uuidv4(),
|
|
||||||
_rev: () => Date.now(),
|
|
||||||
};
|
|
||||||
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
|
|
||||||
const INJ_ORDER = 'injectionOrder';
|
|
||||||
const order = {main: {}, prio: {}};
|
|
||||||
const orderWrap = {
|
|
||||||
id: INJ_ORDER,
|
|
||||||
value: mapObj(order, () => []),
|
|
||||||
_id: `${chrome.runtime.id}-${INJ_ORDER}`,
|
|
||||||
_rev: 0,
|
|
||||||
};
|
|
||||||
uuidIndex.addCustomId(orderWrap, {set: setOrder});
|
|
||||||
|
|
||||||
class MatchQuery {
|
|
||||||
constructor(url) {
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
get urlWithoutHash() {
|
|
||||||
return this._set('urlWithoutHash', this.url.split('#', 1)[0]);
|
|
||||||
}
|
|
||||||
get urlWithoutParams() {
|
|
||||||
return this._set('urlWithoutParams', this.url.split(/[?#]/, 1)[0]);
|
|
||||||
}
|
|
||||||
get domain() {
|
|
||||||
return this._set('domain', tryURL(this.url).hostname);
|
|
||||||
}
|
|
||||||
get isOwnPage() {
|
|
||||||
return this._set('isOwnPage', this.url.startsWith(URLS.ownOrigin));
|
|
||||||
}
|
|
||||||
_set(name, value) {
|
|
||||||
Object.defineProperty(this, name, {value});
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
|
||||||
let ready = Promise.all([init(), prefs.ready]);
|
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
|
||||||
if (port.name === 'livePreview') {
|
|
||||||
handleLivePreview(port);
|
|
||||||
} else if (port.name.startsWith('draft:')) {
|
|
||||||
handleDraft(port);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
colorScheme.onChange(value => {
|
|
||||||
msg.broadcastExtension({method: 'colorScheme', value});
|
|
||||||
for (const {style} of dataMap.values()) {
|
|
||||||
if (colorScheme.SCHEMES.includes(style.preferScheme)) {
|
|
||||||
broadcastStyleUpdated(style, 'colorScheme');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Exports
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
/** @returns {Promise<number>} style id */
|
|
||||||
async delete(id, reason) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
const {style, appliesTo} = dataMap.get(id);
|
|
||||||
const sync = reason !== 'sync';
|
|
||||||
const uuid = style._id;
|
|
||||||
db.styles.delete(id);
|
|
||||||
if (sync) API.sync.delete(uuid, Date.now());
|
|
||||||
for (const url of appliesTo) {
|
|
||||||
const cache = cachedStyleForUrl.get(url);
|
|
||||||
if (cache) delete cache.sections[id];
|
|
||||||
}
|
|
||||||
dataMap.delete(id);
|
|
||||||
uuidIndex.delete(uuid);
|
|
||||||
mapObj(orderWrap.value, (group, type) => {
|
|
||||||
delete order[type][id];
|
|
||||||
const i = group.indexOf(uuid);
|
|
||||||
if (i >= 0) group.splice(i, 1);
|
|
||||||
});
|
|
||||||
setOrder(orderWrap, {calc: false});
|
|
||||||
if (style._usw && style._usw.token) {
|
|
||||||
// Must be called after the style is deleted from dataMap
|
|
||||||
API.usw.revoke(id);
|
|
||||||
}
|
|
||||||
API.drafts.delete(id);
|
|
||||||
await msg.broadcast({
|
|
||||||
method: 'styleDeleted',
|
|
||||||
style: {id},
|
|
||||||
});
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleObj>} */
|
|
||||||
async editSave(style) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
style = mergeWithMapped(style);
|
|
||||||
style.updateDate = Date.now();
|
|
||||||
return saveStyle(style, {reason: 'editSave'});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<?StyleObj>} */
|
|
||||||
async find(...filters) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
for (const filter of filters) {
|
|
||||||
const filterEntries = Object.entries(filter);
|
|
||||||
for (const {style} of dataMap.values()) {
|
|
||||||
if (filterEntries.every(([key, val]) => style[key] === val)) {
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleObj[]>} */
|
|
||||||
async getAll() {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
return getAllAsArray();
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<Object<string,StyleObj[]>>}>} */
|
|
||||||
async getAllOrdered(keys) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
const res = mapObj(orderWrap.value, group => group.map(uuid2style).filter(Boolean));
|
|
||||||
if (res.main.length + res.prio.length < dataMap.size) {
|
|
||||||
for (const {style} of dataMap.values()) {
|
|
||||||
if (!(style.id in order.main) && !(style.id in order.prio)) {
|
|
||||||
res.main.push(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
? mapObj(res, group => group.map(style => mapObj(style, null, keys)))
|
|
||||||
: res;
|
|
||||||
},
|
|
||||||
|
|
||||||
getOrder: () => orderWrap.value,
|
|
||||||
|
|
||||||
/** @returns {Promise<string | {[remoteId:string]: styleId}>}>} */
|
|
||||||
async getRemoteInfo(id) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (id) return calcRemoteId(id2style(id));
|
|
||||||
const res = {};
|
|
||||||
for (const {style} of dataMap.values()) {
|
|
||||||
const [rid, vars] = calcRemoteId(style);
|
|
||||||
if (rid) res[rid] = [style.id, vars];
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleSectionsToApply>} */
|
|
||||||
async getSectionsByUrl(url, id, isInitialApply) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (isInitialApply && prefs.get('disableAll')) {
|
|
||||||
return {
|
|
||||||
cfg: {
|
|
||||||
disableAll: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// TODO: enable in FF when it supports sourceURL comment in style elements (also options.html)
|
|
||||||
const {exposeStyleName} = CHROME && prefs.__values;
|
|
||||||
const sender = CHROME && this && this.sender || {};
|
|
||||||
if (sender.frameId === 0) {
|
|
||||||
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
|
|
||||||
so we'll use the real URL reported by webNavigation API.
|
|
||||||
TODO: if FF will do the same, this won't work as is: FF reports onCommitted too late */
|
|
||||||
url = tabMan.get(sender.tab.id, 'url', 0) || url;
|
|
||||||
}
|
|
||||||
let cache = cachedStyleForUrl.get(url);
|
|
||||||
if (!cache) {
|
|
||||||
cache = {
|
|
||||||
sections: {},
|
|
||||||
maybeMatch: new Set(),
|
|
||||||
};
|
|
||||||
buildCache(cache, url, dataMap.values());
|
|
||||||
cachedStyleForUrl.set(url, cache);
|
|
||||||
} else if (cache.maybeMatch.size) {
|
|
||||||
buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean));
|
|
||||||
}
|
|
||||||
return Object.assign({cfg: {exposeStyleName, order}},
|
|
||||||
id ? mapObj(cache.sections, null, [id])
|
|
||||||
: cache.sections);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleObj>} */
|
|
||||||
async get(id) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
return id2style(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StylesByUrlResult[]>} */
|
|
||||||
async getByUrl(url, id = null) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
// FIXME: do we want to cache this? Who would like to open popup rapidly
|
|
||||||
// or search the DB with the same URL?
|
|
||||||
const result = [];
|
|
||||||
const styles = id
|
|
||||||
? [id2style(id)].filter(Boolean)
|
|
||||||
: getAllAsArray();
|
|
||||||
const query = new MatchQuery(url);
|
|
||||||
for (const style of styles) {
|
|
||||||
let excluded = false;
|
|
||||||
let excludedScheme = false;
|
|
||||||
let included = false;
|
|
||||||
let sloppy = false;
|
|
||||||
let sectionMatched = false;
|
|
||||||
const match = urlMatchStyle(query, style);
|
|
||||||
// TODO: enable this when the function starts returning false
|
|
||||||
// if (match === false) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
if (match === 'included') {
|
|
||||||
included = true;
|
|
||||||
}
|
|
||||||
if (match === 'excluded') {
|
|
||||||
excluded = true;
|
|
||||||
}
|
|
||||||
if (match === 'excludedScheme') {
|
|
||||||
excludedScheme = true;
|
|
||||||
}
|
|
||||||
for (const section of style.sections) {
|
|
||||||
const match = urlMatchSection(query, section, true);
|
|
||||||
if (match) {
|
|
||||||
if (match === 'sloppy') {
|
|
||||||
sloppy = true;
|
|
||||||
}
|
|
||||||
sectionMatched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sectionMatched || included) {
|
|
||||||
result.push(/** @namespace StylesByUrlResult */ {
|
|
||||||
style, excluded, sloppy, excludedScheme, sectionMatched, included});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleObj[]>} */
|
|
||||||
async importMany(items) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
for (const style of items) {
|
|
||||||
beforeSave(style);
|
|
||||||
if (style.sourceCode && style.usercssData) {
|
|
||||||
await usercssMan.buildCode(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const events = await db.styles.putMany(items);
|
|
||||||
return Promise.all(items.map((item, i) =>
|
|
||||||
handleSave(item, {reason: 'import'}, events[i])
|
|
||||||
));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleObj>} */
|
|
||||||
async install(style, reason = null) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
reason = reason || dataMap.has(style.id) ? 'update' : 'install';
|
|
||||||
style = mergeWithMapped(style);
|
|
||||||
style.originalDigest = await calcStyleDigest(style);
|
|
||||||
// FIXME: update updateDate? what about usercss config?
|
|
||||||
return saveStyle(style, {reason});
|
|
||||||
},
|
|
||||||
|
|
||||||
save: saveStyle,
|
|
||||||
|
|
||||||
async setOrder(value) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
return setOrder({value}, {broadcast: true, sync: true});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<number>} style id */
|
|
||||||
async toggle(id, enabled) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
const style = Object.assign({}, id2style(id), {enabled});
|
|
||||||
await saveStyle(style, {reason: 'toggle'});
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
|
|
||||||
// using bind() to skip step-into when debugging
|
|
||||||
|
|
||||||
/** @returns {Promise<StyleObj>} */
|
|
||||||
addExclusion: addIncludeExclude.bind(null, 'exclusions'),
|
|
||||||
/** @returns {Promise<StyleObj>} */
|
|
||||||
addInclusion: addIncludeExclude.bind(null, 'inclusions'),
|
|
||||||
/** @returns {Promise<?StyleObj>} */
|
|
||||||
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
|
|
||||||
/** @returns {Promise<?StyleObj>} */
|
|
||||||
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
|
|
||||||
|
|
||||||
async config(id, prop, value) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
const style = Object.assign({}, id2style(id));
|
|
||||||
const {preview = {}} = dataMap.get(id);
|
|
||||||
style[prop] = preview[prop] = value;
|
|
||||||
return saveStyle(style, {reason: 'config'});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Implementation
|
|
||||||
|
|
||||||
/** @returns {StyleMapData} */
|
|
||||||
function id2data(id) {
|
|
||||||
return dataMap.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {?StyleObj} */
|
|
||||||
function id2style(id) {
|
|
||||||
return (dataMap.get(Number(id)) || {}).style;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {?StyleObj} */
|
|
||||||
function uuid2style(uuid) {
|
|
||||||
return id2style(uuidIndex.get(uuid));
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcRemoteId({md5Url, updateUrl, usercssData: ucd} = {}) {
|
|
||||||
let id;
|
|
||||||
id = (id = /\d+/.test(md5Url) || URLS.extractUsoArchiveId(updateUrl)) && `uso-${id}`
|
|
||||||
|| (id = URLS.extractUSwId(updateUrl)) && `usw-${id}`
|
|
||||||
|| '';
|
|
||||||
return id && [
|
|
||||||
id,
|
|
||||||
ucd && !isEmptyObj(ucd.vars),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {StyleObj} */
|
|
||||||
function createNewStyle() {
|
|
||||||
return /** @namespace StyleObj */ {
|
|
||||||
enabled: true,
|
|
||||||
updateUrl: null,
|
|
||||||
md5Url: null,
|
|
||||||
url: null,
|
|
||||||
originalMd5: null,
|
|
||||||
installDate: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {void} */
|
|
||||||
function storeInMap(style) {
|
|
||||||
dataMap.set(style.id, {
|
|
||||||
style,
|
|
||||||
appliesTo: new Set(),
|
|
||||||
});
|
|
||||||
uuidIndex.set(style._id, style.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {StyleObj} */
|
|
||||||
function mergeWithMapped(style) {
|
|
||||||
return Object.assign({},
|
|
||||||
id2style(style.id) || createNewStyle(),
|
|
||||||
style);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDraft(port) {
|
|
||||||
const id = port.name.split(':').pop();
|
|
||||||
port.onDisconnect.addListener(() => API.drafts.delete(Number(id) || id));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLivePreview(port) {
|
|
||||||
let id;
|
|
||||||
port.onMessage.addListener(style => {
|
|
||||||
if (!id) id = style.id;
|
|
||||||
const data = id2data(id);
|
|
||||||
data.preview = style;
|
|
||||||
broadcastStyleUpdated(style, 'editPreview');
|
|
||||||
});
|
|
||||||
port.onDisconnect.addListener(() => {
|
|
||||||
port = null;
|
|
||||||
if (id) {
|
|
||||||
const data = id2data(id);
|
|
||||||
if (data) {
|
|
||||||
data.preview = null;
|
|
||||||
broadcastStyleUpdated(data.style, 'editPreviewEnd');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addIncludeExclude(type, id, rule) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
const style = Object.assign({}, id2style(id));
|
|
||||||
const list = style[type] || (style[type] = []);
|
|
||||||
if (list.includes(rule)) {
|
|
||||||
throw new Error('The rule already exists');
|
|
||||||
}
|
|
||||||
style[type] = list.concat([rule]);
|
|
||||||
return saveStyle(style, {reason: 'config'});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeIncludeExclude(type, id, rule) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
const style = Object.assign({}, id2style(id));
|
|
||||||
const list = style[type];
|
|
||||||
if (!list || !list.includes(rule)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
style[type] = list.filter(r => r !== rule);
|
|
||||||
return saveStyle(style, {reason: 'config'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function broadcastStyleUpdated(style, reason, method = 'styleUpdated') {
|
|
||||||
const {id} = style;
|
|
||||||
const data = id2data(id);
|
|
||||||
const excluded = new Set();
|
|
||||||
const updated = new Set();
|
|
||||||
for (const [url, cache] of cachedStyleForUrl.entries()) {
|
|
||||||
if (!data.appliesTo.has(url)) {
|
|
||||||
cache.maybeMatch.add(id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const code = getAppliedCode(new MatchQuery(url), style);
|
|
||||||
if (code) {
|
|
||||||
updated.add(url);
|
|
||||||
buildCacheEntry(cache, style, code);
|
|
||||||
} else {
|
|
||||||
excluded.add(url);
|
|
||||||
delete cache.sections[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.appliesTo = updated;
|
|
||||||
return msg.broadcast({
|
|
||||||
method,
|
|
||||||
reason,
|
|
||||||
style: {
|
|
||||||
id,
|
|
||||||
md5Url: style.md5Url,
|
|
||||||
enabled: style.enabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function beforeSave(style) {
|
|
||||||
if (!style.name) {
|
|
||||||
throw new Error('Style name is empty');
|
|
||||||
}
|
|
||||||
for (const key of DELETE_IF_NULL) {
|
|
||||||
if (style[key] == null) {
|
|
||||||
delete style[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!style._id) {
|
|
||||||
style._id = uuidv4();
|
|
||||||
}
|
|
||||||
style._rev = Date.now();
|
|
||||||
fixKnownProblems(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveStyle(style, handlingOptions) {
|
|
||||||
beforeSave(style);
|
|
||||||
const newId = await db.styles.put(style);
|
|
||||||
return handleSave(style, handlingOptions, newId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSave(style, {reason, broadcast = true}, id = style.id) {
|
|
||||||
if (style.id == null) style.id = id;
|
|
||||||
const data = id2data(id);
|
|
||||||
const method = data ? 'styleUpdated' : 'styleAdded';
|
|
||||||
if (!data) {
|
|
||||||
storeInMap(style);
|
|
||||||
} else {
|
|
||||||
data.style = style;
|
|
||||||
}
|
|
||||||
if (reason !== 'sync') {
|
|
||||||
API.sync.putDoc(style);
|
|
||||||
}
|
|
||||||
if (broadcast) broadcastStyleUpdated(style, reason, method);
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get styles matching a URL, including sloppy regexps and excluded items.
|
|
||||||
function getAppliedCode(query, data) {
|
|
||||||
const result = urlMatchStyle(query, data);
|
|
||||||
if (result === 'included') {
|
|
||||||
// return all sections
|
|
||||||
return data.sections.map(s => s.code);
|
|
||||||
}
|
|
||||||
if (result !== true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const code = [];
|
|
||||||
for (const section of data.sections) {
|
|
||||||
if (urlMatchSection(query, section) === true && !styleCodeEmpty(section.code)) {
|
|
||||||
code.push(section.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return code.length && code;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const orderPromise = API.prefsDb.get(orderWrap.id);
|
|
||||||
const styles = await db.styles.getAll() || [];
|
|
||||||
const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean));
|
|
||||||
if (updated.length) {
|
|
||||||
await db.styles.putMany(updated);
|
|
||||||
}
|
|
||||||
setOrder(await orderPromise, {store: false});
|
|
||||||
styles.forEach(storeInMap);
|
|
||||||
ready = true;
|
|
||||||
bgReady._resolveStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixKnownProblems(style, initIndex, initArray) {
|
|
||||||
let res = 0;
|
|
||||||
for (const key in MISSING_PROPS) {
|
|
||||||
if (!style[key]) {
|
|
||||||
style[key] = MISSING_PROPS[key](style);
|
|
||||||
res = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Upgrade the old way of customizing local names */
|
|
||||||
const {originalName} = style;
|
|
||||||
if (originalName) {
|
|
||||||
if (originalName !== style.name) {
|
|
||||||
style.customName = style.name;
|
|
||||||
style.name = originalName;
|
|
||||||
}
|
|
||||||
delete style.originalName;
|
|
||||||
res = 1;
|
|
||||||
}
|
|
||||||
/* wrong homepage url in 1.5.20-1.5.21 due to commit 1e5f118d */
|
|
||||||
for (const key of ['url', 'installationUrl']) {
|
|
||||||
const url = style[key];
|
|
||||||
const fixedUrl = url && url.replace(/([^:]\/)\//, '$1');
|
|
||||||
if (fixedUrl !== url) {
|
|
||||||
res = 1;
|
|
||||||
style[key] = fixedUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let url;
|
|
||||||
/* USO bug, duplicate "update" subdomain, see #523 */
|
|
||||||
if ((url = style.md5Url) && url.includes('update.update.userstyles')) {
|
|
||||||
res = style.md5Url = url.replace('update.update.userstyles', 'update.userstyles');
|
|
||||||
}
|
|
||||||
/* Default homepage URL for external styles installed from a known distro */
|
|
||||||
if (
|
|
||||||
(!style.url || !style.installationUrl) &&
|
|
||||||
(url = style.updateUrl) &&
|
|
||||||
(url = URLS.extractGreasyForkInstallUrl(url) ||
|
|
||||||
URLS.extractUsoArchiveInstallUrl(url) ||
|
|
||||||
URLS.extractUSwInstallUrl(url)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (!style.url) res = style.url = url;
|
|
||||||
if (!style.installationUrl) res = style.installationUrl = url;
|
|
||||||
}
|
|
||||||
/* @import must precede `vars` that we add at beginning */
|
|
||||||
if (
|
|
||||||
initArray &&
|
|
||||||
!isEmptyObj((style.usercssData || {}).vars) &&
|
|
||||||
style.sections.some(({code}) =>
|
|
||||||
code.startsWith(':root {\n --') &&
|
|
||||||
/@import\s/i.test(code))
|
|
||||||
) {
|
|
||||||
return usercssMan.buildCode(style);
|
|
||||||
}
|
|
||||||
return res && style;
|
|
||||||
}
|
|
||||||
|
|
||||||
function urlMatchStyle(query, style) {
|
|
||||||
if (
|
|
||||||
style.exclusions &&
|
|
||||||
style.exclusions.some(e => compileExclusion(e).test(query.urlWithoutParams))
|
|
||||||
) {
|
|
||||||
return 'excluded';
|
|
||||||
}
|
|
||||||
if (!style.enabled) {
|
|
||||||
return 'disabled';
|
|
||||||
}
|
|
||||||
if (!colorScheme.shouldIncludeStyle(style)) {
|
|
||||||
return 'excludedScheme';
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
style.inclusions &&
|
|
||||||
style.inclusions.some(r => compileExclusion(r).test(query.urlWithoutParams))
|
|
||||||
) {
|
|
||||||
return 'included';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function urlMatchSection(query, section, skipEmptyGlobal) {
|
|
||||||
let dd, ddL, pp, ppL, rr, rrL, uu, uuL;
|
|
||||||
if (
|
|
||||||
(dd = section.domains) && (ddL = dd.length) && dd.some(urlMatchDomain, query) ||
|
|
||||||
(pp = section.urlPrefixes) && (ppL = pp.length) && pp.some(urlMatchPrefix, query) ||
|
|
||||||
/* Per the specification the fragment portion is ignored in @-moz-document:
|
|
||||||
https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
|
||||||
but the spec is outdated and doesn't account for SPA sites,
|
|
||||||
so we only respect it for `url()` function */
|
|
||||||
(uu = section.urls) && (uuL = uu.length) && (
|
|
||||||
uu.includes(query.url) ||
|
|
||||||
uu.includes(query.urlWithoutHash)
|
|
||||||
) ||
|
|
||||||
(rr = section.regexps) && (rrL = rr.length) && rr.some(urlMatchRegexp, query)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
According to CSS4 @document specification the entire URL must match.
|
|
||||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
|
||||||
We'll detect styles that abuse the bug by finding the sections that
|
|
||||||
would have been applied by Stylish but not by us as we follow the spec.
|
|
||||||
*/
|
|
||||||
if (rrL && rr.some(urlMatchRegexpSloppy, query)) {
|
|
||||||
return 'sloppy';
|
|
||||||
}
|
|
||||||
// TODO: check for invalid regexps?
|
|
||||||
return !rrL && !ppL && !uuL && !ddL &&
|
|
||||||
!query.isOwnPage && // We allow only intentionally targeted sections for own pages
|
|
||||||
(!skipEmptyGlobal || !styleCodeEmpty(section.code));
|
|
||||||
}
|
|
||||||
/** @this {MatchQuery} */
|
|
||||||
function urlMatchDomain(d) {
|
|
||||||
const _d = this.domain;
|
|
||||||
return d === _d ||
|
|
||||||
_d[_d.length - d.length - 1] === '.' && _d.endsWith(d);
|
|
||||||
}
|
|
||||||
/** @this {MatchQuery} */
|
|
||||||
function urlMatchPrefix(p) {
|
|
||||||
return p && this.url.startsWith(p);
|
|
||||||
}
|
|
||||||
/** @this {MatchQuery} */
|
|
||||||
function urlMatchRegexp(r) {
|
|
||||||
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
|
|
||||||
compileRe(r).test(this.url);
|
|
||||||
}
|
|
||||||
/** @this {MatchQuery} */
|
|
||||||
function urlMatchRegexpSloppy(r) {
|
|
||||||
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
|
|
||||||
compileSloppyRe(r).test(this.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCompiler(compile) {
|
|
||||||
// FIXME: FIFO cache doesn't work well here, if we want to match many
|
|
||||||
// regexps more than the cache size, we will never hit the cache because
|
|
||||||
// the first cache is deleted. So we use a simple map but it leaks memory.
|
|
||||||
const cache = new Map();
|
|
||||||
return text => {
|
|
||||||
let re = cache.get(text);
|
|
||||||
if (!re) {
|
|
||||||
re = tryRegExp(compile(text));
|
|
||||||
if (!re) {
|
|
||||||
re = BAD_MATCHER;
|
|
||||||
}
|
|
||||||
cache.set(text, re);
|
|
||||||
}
|
|
||||||
return re;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileGlob(text) {
|
|
||||||
return stringAsRegExp(text, '', true)
|
|
||||||
.replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*');
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildExclusion(text) {
|
|
||||||
// match pattern
|
|
||||||
const match = text.match(/^(\*|[\w-]+):\/\/(\*\.)?([\w.]+\/.*)/);
|
|
||||||
if (!match) {
|
|
||||||
return '^' + compileGlob(text) + '$';
|
|
||||||
}
|
|
||||||
return '^' +
|
|
||||||
(match[1] === '*' ? '[\\w-]+' : match[1]) +
|
|
||||||
'://' +
|
|
||||||
(match[2] ? '(?:[\\w.]+\\.)?' : '') +
|
|
||||||
compileGlob(match[3]) +
|
|
||||||
'$';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCache(cache, url, styleList) {
|
|
||||||
const query = new MatchQuery(url);
|
|
||||||
for (const {style, appliesTo, preview} of styleList) {
|
|
||||||
const code = getAppliedCode(query, preview || style);
|
|
||||||
if (code) {
|
|
||||||
buildCacheEntry(cache, style, code);
|
|
||||||
appliesTo.add(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCacheEntry(cache, style, code = style.code) {
|
|
||||||
cache.sections[style.id] = {
|
|
||||||
code,
|
|
||||||
id: style.id,
|
|
||||||
name: style.customName || style.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {StyleObj[]} */
|
|
||||||
function getAllAsArray() {
|
|
||||||
return Array.from(dataMap.values(), v => v.style);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */
|
|
||||||
function hex4dashed(num, i) {
|
|
||||||
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setOrder(data, {broadcast, calc = true, store = true, sync} = {}) {
|
|
||||||
if (!data || !data.value || deepEqual(data.value, orderWrap.value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.assign(orderWrap, data, sync && {_rev: Date.now()});
|
|
||||||
if (calc) {
|
|
||||||
for (const [type, group] of Object.entries(data.value)) {
|
|
||||||
const dst = order[type] = {};
|
|
||||||
group.forEach((uuid, i) => {
|
|
||||||
const id = uuidIndex.get(uuid);
|
|
||||||
if (id) dst[id] = i;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (broadcast) {
|
|
||||||
msg.broadcast({method: 'styleSort', order});
|
|
||||||
}
|
|
||||||
if (store) {
|
|
||||||
await API.prefsDb.put(orderWrap, orderWrap.id);
|
|
||||||
}
|
|
||||||
if (sync) {
|
|
||||||
API.sync.putDoc(orderWrap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
})();
|
|
|
@ -1,108 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
/* global RX_META debounce stringAsRegExp tryRegExp */// toolbox.js
|
|
||||||
/* global addAPI */// common.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
// toLocaleLowerCase cache, autocleared after 1 minute
|
|
||||||
const cache = new Map();
|
|
||||||
const METAKEYS = ['customName', 'name', 'url', 'installationUrl', 'updateUrl'];
|
|
||||||
|
|
||||||
const extractMeta = style =>
|
|
||||||
style.usercssData
|
|
||||||
? (style.sourceCode.match(RX_META) || [''])[0]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const stripMeta = style =>
|
|
||||||
style.usercssData
|
|
||||||
? style.sourceCode.replace(RX_META, '')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const MODES = Object.assign(Object.create(null), {
|
|
||||||
code: (style, test) =>
|
|
||||||
style.usercssData
|
|
||||||
? test(stripMeta(style))
|
|
||||||
: searchSections(style, test, 'code'),
|
|
||||||
|
|
||||||
meta: (style, test, part) =>
|
|
||||||
METAKEYS.some(key => test(style[key])) ||
|
|
||||||
test(part === 'all' ? style.sourceCode : extractMeta(style)) ||
|
|
||||||
searchSections(style, test, 'funcs'),
|
|
||||||
|
|
||||||
name: (style, test) =>
|
|
||||||
test(style.customName) ||
|
|
||||||
test(style.name),
|
|
||||||
|
|
||||||
all: (style, test) =>
|
|
||||||
MODES.meta(style, test, 'all') ||
|
|
||||||
!style.usercssData && MODES.code(style, test),
|
|
||||||
});
|
|
||||||
|
|
||||||
addAPI(/** @namespace API */ {
|
|
||||||
styles: {
|
|
||||||
/**
|
|
||||||
* @param params
|
|
||||||
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
|
|
||||||
* @param {'name'|'meta'|'code'|'all'|'url'} [params.mode=all]
|
|
||||||
* @param {number[]} [params.ids] - if not specified, all styles are searched
|
|
||||||
* @returns {number[]} - array of matched styles ids
|
|
||||||
*/
|
|
||||||
async searchDB({query, mode = 'all', ids}) {
|
|
||||||
let res = [];
|
|
||||||
if (mode === 'url' && query) {
|
|
||||||
res = (await API.styles.getByUrl(query)).map(r => r.style.id);
|
|
||||||
} else if (mode in MODES) {
|
|
||||||
const modeHandler = MODES[mode];
|
|
||||||
const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
|
|
||||||
const rx = m && tryRegExp(m[1], m[2]);
|
|
||||||
const test = rx ? rx.test.bind(rx) : createTester(query);
|
|
||||||
res = (await API.styles.getAll())
|
|
||||||
.filter(style =>
|
|
||||||
(!ids || ids.includes(style.id)) &&
|
|
||||||
(!query || modeHandler(style, test)))
|
|
||||||
.map(style => style.id);
|
|
||||||
if (cache.size) debounce(clearCache, 60e3);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function createTester(query) {
|
|
||||||
const flags = `u${lower(query) === query ? 'i' : ''}`;
|
|
||||||
const words = query
|
|
||||||
.split(/(".*?")|\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map(w => w.startsWith('"') && w.endsWith('"')
|
|
||||||
? w.slice(1, -1)
|
|
||||||
: w)
|
|
||||||
.filter(w => w.length > 1);
|
|
||||||
const rxs = (words.length ? words : [query])
|
|
||||||
.map(w => stringAsRegExp(w, flags));
|
|
||||||
return text => rxs.every(rx => rx.test(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchSections({sections}, test, part) {
|
|
||||||
const inCode = part === 'code' || part === 'all';
|
|
||||||
const inFuncs = part === 'funcs' || part === 'all';
|
|
||||||
for (const section of sections) {
|
|
||||||
for (const prop in section) {
|
|
||||||
const value = section[prop];
|
|
||||||
if (inCode && prop === 'code' && test(value) ||
|
|
||||||
inFuncs && Array.isArray(value) && value.some(str => test(str))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function lower(text) {
|
|
||||||
let result = cache.get(text);
|
|
||||||
if (!result) cache.set(text, result = text.toLocaleLowerCase());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearCache() {
|
|
||||||
cache.clear();
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,234 +1,220 @@
|
||||||
/* global API */// msg.js
|
/* global getStyles */
|
||||||
/* global addAPI */// common.js
|
|
||||||
/* global isEmptyObj */// toolbox.js
|
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
// eslint-disable-next-line no-var
|
||||||
* Uses chrome.tabs.insertCSS
|
var styleViaAPI = !CHROME &&
|
||||||
*/
|
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
styleApply,
|
styleApply,
|
||||||
styleDeleted,
|
styleDeleted,
|
||||||
styleUpdated,
|
styleUpdated,
|
||||||
styleAdded,
|
styleAdded,
|
||||||
styleReplaceAll,
|
styleReplaceAll: styleApply,
|
||||||
prefChanged,
|
prefChanged,
|
||||||
updateCount,
|
ping,
|
||||||
};
|
};
|
||||||
const NOP = new Error('NOP');
|
const NOP = Promise.resolve(new Error('NOP'));
|
||||||
const onError = () => {};
|
const PONG = Promise.resolve(true);
|
||||||
/* <tabId>: Object
|
const onError = () => NOP;
|
||||||
<frameId>: Object
|
|
||||||
url: String, non-enumerable
|
|
||||||
<styleId>: Array of strings
|
|
||||||
section code */
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
let observingTabs = false;
|
const allFrameUrls = new Map();
|
||||||
|
|
||||||
addAPI(/** @namespace API */ {
|
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
||||||
async styleViaAPI(request) {
|
chrome.tabs.onReplaced.addListener(onTabReplaced);
|
||||||
try {
|
|
||||||
const fn = ACTIONS[request.method];
|
|
||||||
return fn ? fn(request, this.sender) : NOP;
|
|
||||||
} catch (e) {}
|
|
||||||
maybeToggleObserver();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateCount(request, sender) {
|
return {
|
||||||
const {tab, frameId} = sender;
|
process,
|
||||||
if (frameId) {
|
getFrameUrl,
|
||||||
throw new Error('we do not count styles for frames');
|
setFrameUrl,
|
||||||
}
|
allFrameUrls,
|
||||||
const {frameStyles} = getCachedData(tab.id, frameId);
|
cache,
|
||||||
API.updateIconBadge.call({sender}, Object.keys(frameStyles));
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
//region public methods
|
||||||
if (prefs.get('disableAll')) {
|
|
||||||
|
function process(request, sender) {
|
||||||
|
console.log(request.action || request.method, request.prefs || request.styles || request.style, sender.tab, sender.frameId);
|
||||||
|
const action = ACTIONS[request.action || request.method];
|
||||||
|
if (!action) {
|
||||||
return NOP;
|
return NOP;
|
||||||
}
|
}
|
||||||
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
const {tab} = sender;
|
||||||
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
if (!isNaN(sender.frameId)) {
|
||||||
return NOP;
|
const result = action(request, sender);
|
||||||
|
return result ? result.catch(onError) : NOP;
|
||||||
}
|
}
|
||||||
return API.styles.getSectionsByUrl(url, id).then(sections => {
|
return browser.webNavigation.getAllFrames({tabId: tab.id}).then(frames =>
|
||||||
delete sections.cfg;
|
Promise.all((frames || []).map(({frameId}) =>
|
||||||
const tasks = [];
|
(action(request, {tab, frameId}) || NOP).catch(onError)))
|
||||||
for (const section of Object.values(sections)) {
|
).catch(onError);
|
||||||
const styleId = section.id;
|
|
||||||
const code = section.code.join('\n');
|
|
||||||
if (code === (frameStyles[styleId] || []).join('\n')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
frameStyles[styleId] = section.code;
|
|
||||||
tasks.push(
|
|
||||||
browser.tabs.insertCSS(tab.id, {
|
|
||||||
code,
|
|
||||||
frameId,
|
|
||||||
runAt: 'document_start',
|
|
||||||
matchAboutBlank: true,
|
|
||||||
}).catch(onError));
|
|
||||||
}
|
|
||||||
if (!removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles)) {
|
|
||||||
Object.defineProperty(frameStyles, 'url', {value: url, configurable: true});
|
|
||||||
tabFrames[frameId] = frameStyles;
|
|
||||||
cache.set(tab.id, tabFrames);
|
|
||||||
}
|
|
||||||
return Promise.all(tasks);
|
|
||||||
})
|
|
||||||
.then(() => updateCount(null, {tab, frameId}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleDeleted({style: {id}}, {tab, frameId}) {
|
function getFrameUrl(tabId, frameId = 0) {
|
||||||
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
|
const frameUrls = allFrameUrls.get(tabId);
|
||||||
const code = styleSections.join('\n');
|
return frameUrls && frameUrls[frameId] || '';
|
||||||
if (code && !duplicateCodeExists({frameStyles, id, code})) {
|
}
|
||||||
delete frameStyles[id];
|
|
||||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
|
function setFrameUrl(tabId, frameId, url) {
|
||||||
return removeCSS(tab.id, frameId, code)
|
const frameUrls = allFrameUrls.get(tabId);
|
||||||
.then(() => updateCount(null, {tab, frameId}));
|
if (frameUrls) {
|
||||||
|
frameUrls[frameId] = url;
|
||||||
} else {
|
} else {
|
||||||
return NOP;
|
allFrameUrls.set(tabId, {[frameId]: url});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region actions
|
||||||
|
|
||||||
|
function styleApply({styles, disableAll}, sender) {
|
||||||
|
if (disableAll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {tab: {id: tabId}, frameId, url} = sender;
|
||||||
|
if (!styles || styles === 'DIY') {
|
||||||
|
return requestStyles({matchUrl: url || getFrameUrl(tabId, frameId)}, sender);
|
||||||
|
}
|
||||||
|
const {tabFrames, frameStyles} = getCachedData(tabId, frameId);
|
||||||
|
const newSorted = getSortedById(styles);
|
||||||
|
if (!sameArrays(frameStyles, newSorted, sameArrays)) {
|
||||||
|
tabFrames[frameId] = newSorted;
|
||||||
|
cache.set(tabId, tabFrames);
|
||||||
|
return replaceCSS(tabId, frameId, frameStyles, newSorted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleDeleted({id}, {tab, frameId}) {
|
||||||
|
const {frameStyles} = getCachedData(tab.id, frameId);
|
||||||
|
const index = frameStyles.findIndex(item => item.id === id);
|
||||||
|
if (index >= 0) {
|
||||||
|
const oldStyles = frameStyles.slice();
|
||||||
|
frameStyles.splice(index, 1);
|
||||||
|
return replaceCSS(tab.id, frameId, oldStyles, frameStyles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleUpdated({style}, sender) {
|
function styleUpdated({style}, sender) {
|
||||||
if (!style.enabled) {
|
return (style.enabled ? styleApply : styleDeleted)(style, sender);
|
||||||
return styleDeleted({style}, sender);
|
|
||||||
}
|
|
||||||
const {tab, frameId} = sender;
|
|
||||||
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
|
|
||||||
const code = styleSections.join('\n');
|
|
||||||
return styleApply(style, sender).then(code && (() => {
|
|
||||||
if (!duplicateCodeExists({frameStyles, code, id: null})) {
|
|
||||||
return removeCSS(tab.id, frameId, code);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleAdded({style}, sender) {
|
function styleAdded({style: {enabled}}, sender) {
|
||||||
return style.enabled ? styleApply(style, sender) : NOP;
|
return enabled && styleApply({}, sender);
|
||||||
}
|
|
||||||
|
|
||||||
function styleReplaceAll(request, sender) {
|
|
||||||
const {tab, frameId} = sender;
|
|
||||||
const oldStylesCode = getFrameStylesJoined(sender);
|
|
||||||
return styleApply({ignoreUrlCheck: true}, sender).then(() => {
|
|
||||||
const newStylesCode = getFrameStylesJoined(sender);
|
|
||||||
const tasks = oldStylesCode
|
|
||||||
.filter(code => !newStylesCode.includes(code))
|
|
||||||
.map(code => removeCSS(tab.id, frameId, code));
|
|
||||||
return Promise.all(tasks);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefChanged({prefs}, sender) {
|
function prefChanged({prefs}, sender) {
|
||||||
if ('disableAll' in prefs) {
|
if ('disableAll' in prefs) {
|
||||||
if (!prefs.disableAll) {
|
disableAll(prefs.disableAll, sender);
|
||||||
return styleApply({}, sender);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ping() {
|
||||||
|
return PONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region action helpers
|
||||||
|
|
||||||
|
function disableAll(state, sender) {
|
||||||
|
if (state) {
|
||||||
const {tab, frameId} = sender;
|
const {tab, frameId} = sender;
|
||||||
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
||||||
if (isEmptyObj(frameStyles)) {
|
|
||||||
return NOP;
|
|
||||||
}
|
|
||||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
|
|
||||||
const tasks = Object.keys(frameStyles)
|
|
||||||
.map(id => removeCSS(tab.id, frameId, frameStyles[id].join('\n')));
|
|
||||||
return Promise.all(tasks);
|
|
||||||
} else {
|
|
||||||
return NOP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* utilities */
|
|
||||||
|
|
||||||
function maybeToggleObserver() {
|
|
||||||
let method;
|
|
||||||
if (!observingTabs && cache.size) {
|
|
||||||
method = 'addListener';
|
|
||||||
} else if (observingTabs && !cache.size) {
|
|
||||||
method = 'removeListener';
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
observingTabs = !observingTabs;
|
|
||||||
chrome.webNavigation.onCommitted[method](onNavigationCommitted);
|
|
||||||
chrome.tabs.onRemoved[method](onTabRemoved);
|
|
||||||
chrome.tabs.onReplaced[method](onTabReplaced);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onNavigationCommitted({tabId, frameId}) {
|
|
||||||
if (frameId === 0) {
|
|
||||||
onTabRemoved(tabId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tabFrames = cache.get(tabId);
|
|
||||||
if (tabFrames && frameId in tabFrames) {
|
|
||||||
delete tabFrames[frameId];
|
delete tabFrames[frameId];
|
||||||
if (isEmptyObj(tabFrames)) {
|
return removeCSS(tab.id, frameId, frameStyles);
|
||||||
onTabRemoved(tabId);
|
} else {
|
||||||
}
|
return styleApply({}, sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region observer
|
||||||
|
|
||||||
function onTabRemoved(tabId) {
|
function onTabRemoved(tabId) {
|
||||||
cache.delete(tabId);
|
cache.delete(tabId);
|
||||||
maybeToggleObserver();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTabReplaced(addedTabId, removedTabId) {
|
function onTabReplaced(addedTabId, removedTabId) {
|
||||||
onTabRemoved(removedTabId);
|
cache.delete(removedTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
|
//endregion
|
||||||
if (isEmptyObj(frameStyles)) {
|
//region browser API
|
||||||
delete tabFrames[frameId];
|
|
||||||
if (isEmptyObj(tabFrames)) {
|
function replaceCSS(tabId, frameId, oldStyles, newStyles) {
|
||||||
cache.delete(tabId);
|
console.log.apply(null, arguments);
|
||||||
|
return insertCSS(tabId, frameId, newStyles).then(() =>
|
||||||
|
removeCSS(tabId, frameId, oldStyles));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertCSS(tabId, frameId, frameStyles) {
|
||||||
|
const code = getFrameCode(frameStyles);
|
||||||
|
return !code ? NOP :
|
||||||
|
browser.tabs.insertCSS(tabId, {
|
||||||
|
code,
|
||||||
|
frameId,
|
||||||
|
runAt: 'document_start',
|
||||||
|
matchAboutBlank: true,
|
||||||
|
}).catch(onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCSS(tabId, frameId, frameStyles) {
|
||||||
|
const code = getFrameCode(frameStyles);
|
||||||
|
return !code ? NOP :
|
||||||
|
browser.tabs.removeCSS(tabId, {
|
||||||
|
code,
|
||||||
|
frameId,
|
||||||
|
matchAboutBlank: true
|
||||||
|
}).catch(onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region utilities
|
||||||
|
|
||||||
|
function requestStyles(options, sender) {
|
||||||
|
options.matchUrl = options.matchUrl || sender.url;
|
||||||
|
options.enabled = true;
|
||||||
|
options.asHash = true;
|
||||||
|
return getStyles(options).then(styles =>
|
||||||
|
styleApply({styles}, sender));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortedById(styleHash) {
|
||||||
|
const styles = [];
|
||||||
|
let needsSorting = false;
|
||||||
|
let prevKey = -1;
|
||||||
|
for (let k in styleHash) {
|
||||||
|
k = parseInt(k);
|
||||||
|
if (!isNaN(k)) {
|
||||||
|
const sections = styleHash[k].map(({code}) => code);
|
||||||
|
styles.push(sections);
|
||||||
|
defineProperty(sections, 'id', k);
|
||||||
|
needsSorting |= k < prevKey;
|
||||||
|
prevKey = k;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
return needsSorting ? styles.sort((a, b) => a.id - b.id) : styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCachedData(tabId, frameId, styleId) {
|
function getCachedData(tabId, frameId, styleId) {
|
||||||
const tabFrames = cache.get(tabId) || {};
|
const tabFrames = cache.get(tabId) || {};
|
||||||
const frameStyles = tabFrames[frameId] || {};
|
const frameStyles = tabFrames[frameId] || [];
|
||||||
const styleSections = styleId && frameStyles[styleId] || [];
|
const styleSections = styleId && frameStyles.find(s => s.id === styleId) || [];
|
||||||
return {tabFrames, frameStyles, styleSections};
|
return {tabFrames, frameStyles, styleSections};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFrameStylesJoined({
|
function getFrameCode(frameStyles) {
|
||||||
tab,
|
// we cache a shallow copy of code from the sections array in order to reuse references
|
||||||
frameId,
|
// in other places whereas the combined string gets garbage-collected
|
||||||
frameStyles = getCachedData(tab.id, frameId).frameStyles,
|
return typeof frameStyles === 'string' ? frameStyles : [].concat(...frameStyles).join('\n');
|
||||||
}) {
|
|
||||||
return Object.keys(frameStyles).map(id => frameStyles[id].join('\n'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateCodeExists({
|
function defineProperty(obj, name, value) {
|
||||||
tab,
|
return Object.defineProperty(obj, name, {value, configurable: true});
|
||||||
frameId,
|
|
||||||
frameStyles = getCachedData(tab.id, frameId).frameStyles,
|
|
||||||
frameStylesCode = {},
|
|
||||||
id,
|
|
||||||
code = frameStylesCode[id] || frameStyles[id].join('\n'),
|
|
||||||
}) {
|
|
||||||
id = String(id);
|
|
||||||
for (const styleId in frameStyles) {
|
|
||||||
if (id !== styleId &&
|
|
||||||
code === (frameStylesCode[styleId] || frameStyles[styleId].join('\n'))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCSS(tabId, frameId, code) {
|
function sameArrays(a, b, fn) {
|
||||||
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
|
return a.length === b.length && a.every((el, i) => fn ? fn(el, b[i]) : el === b[i]);
|
||||||
.catch(onError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
/* global CHROME URLS ignoreChromeError */// toolbox.js
|
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const idCSP = 'patchCsp';
|
|
||||||
const idOFF = 'disableAll';
|
|
||||||
const idXHR = 'styleViaXhr';
|
|
||||||
const rxHOST = /^('none'|(https?:\/\/)?[^']+?[^:'])$/; // strips CSP sources covered by *
|
|
||||||
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
|
|
||||||
/** @type {Object<string,StylesToPass>} */
|
|
||||||
const stylesToPass = {};
|
|
||||||
const state = {};
|
|
||||||
const injectedCode = CHROME && `${data => {
|
|
||||||
if (self.INJECTED !== 1) { // storing data only if apply.js hasn't run yet
|
|
||||||
window[Symbol.for('styles')] = data;
|
|
||||||
}
|
|
||||||
}}`;
|
|
||||||
|
|
||||||
toggle();
|
|
||||||
prefs.subscribe([idXHR, idOFF, idCSP], toggle);
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
const off = prefs.get(idOFF);
|
|
||||||
const csp = prefs.get(idCSP) && !off;
|
|
||||||
const xhr = prefs.get(idXHR) && !off;
|
|
||||||
if (xhr === state.xhr && csp === state.csp && off === state.off) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reqFilter = {
|
|
||||||
urls: ['*://*/*'],
|
|
||||||
types: ['main_frame', 'sub_frame'],
|
|
||||||
};
|
|
||||||
chrome.webNavigation.onCommitted.removeListener(injectData);
|
|
||||||
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
|
|
||||||
chrome.webRequest.onHeadersReceived.removeListener(modifyHeaders);
|
|
||||||
if (xhr || csp) {
|
|
||||||
// We unregistered it above so that the optional EXTRA_HEADERS is properly re-registered
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(modifyHeaders, reqFilter, [
|
|
||||||
'blocking',
|
|
||||||
'responseHeaders',
|
|
||||||
xhr && chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
|
|
||||||
].filter(Boolean));
|
|
||||||
}
|
|
||||||
if (CHROME ? !off : xhr || csp) {
|
|
||||||
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
|
|
||||||
}
|
|
||||||
if (CHROME && !off) {
|
|
||||||
chrome.webNavigation.onCommitted.addListener(injectData, {url: [{urlPrefix: 'http'}]});
|
|
||||||
}
|
|
||||||
if (CHROME) {
|
|
||||||
chrome.webRequest.onBeforeRequest.addListener(openNamedStyle, {
|
|
||||||
urls: [URLS.ownOrigin + '*.user.css'],
|
|
||||||
types: ['main_frame'],
|
|
||||||
}, ['blocking']);
|
|
||||||
}
|
|
||||||
state.csp = csp;
|
|
||||||
state.off = off;
|
|
||||||
state.xhr = xhr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
|
|
||||||
async function prepareStyles(req) {
|
|
||||||
const sections = await API.styles.getSectionsByUrl(req.url);
|
|
||||||
stylesToPass[req2key(req)] = /** @namespace StylesToPass */ {
|
|
||||||
blobId: '',
|
|
||||||
str: JSON.stringify(sections),
|
|
||||||
timer: setTimeout(cleanUp, 600e3, req),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectData(req) {
|
|
||||||
const data = stylesToPass[req2key(req)];
|
|
||||||
if (data && !data.injected) {
|
|
||||||
data.injected = true;
|
|
||||||
chrome.tabs.executeScript(req.tabId, {
|
|
||||||
frameId: req.frameId,
|
|
||||||
runAt: 'document_start',
|
|
||||||
code: `(${injectedCode})(${data.str})`,
|
|
||||||
}, ignoreChromeError);
|
|
||||||
if (!state.xhr) cleanUp(req);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
|
|
||||||
function modifyHeaders(req) {
|
|
||||||
const {responseHeaders} = req;
|
|
||||||
const data = stylesToPass[req2key(req)];
|
|
||||||
if (!data || data.str === '{}') {
|
|
||||||
cleanUp(req);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.xhr) {
|
|
||||||
data.blobId = URL.createObjectURL(new Blob([data.str])).slice(blobUrlPrefix.length);
|
|
||||||
responseHeaders.push({
|
|
||||||
name: 'Set-Cookie',
|
|
||||||
value: `${chrome.runtime.id}=${data.blobId}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const csp = state.csp &&
|
|
||||||
responseHeaders.find(h => h.name.toLowerCase() === 'content-security-policy');
|
|
||||||
if (csp) {
|
|
||||||
patchCsp(csp);
|
|
||||||
}
|
|
||||||
if (state.xhr || csp) {
|
|
||||||
return {responseHeaders};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.HttpHeader} csp */
|
|
||||||
function patchCsp(csp) {
|
|
||||||
const src = {};
|
|
||||||
for (let p of csp.value.split(';')) {
|
|
||||||
p = p.trim().split(/\s+/);
|
|
||||||
src[p[0]] = p.slice(1);
|
|
||||||
}
|
|
||||||
// Allow style assets
|
|
||||||
patchCspSrc(src, 'img-src', 'data:', '*');
|
|
||||||
patchCspSrc(src, 'font-src', 'data:', '*');
|
|
||||||
// Allow our DOM styles, allow @import from any URL
|
|
||||||
patchCspSrc(src, 'style-src', "'unsafe-inline'", '*');
|
|
||||||
// Allow our XHR cookies in CSP sandbox (known case: raw github urls)
|
|
||||||
if (src.sandbox && !src.sandbox.includes('allow-same-origin')) {
|
|
||||||
src.sandbox.push('allow-same-origin');
|
|
||||||
}
|
|
||||||
csp.value = Object.entries(src).map(([k, v]) =>
|
|
||||||
`${k}${v.length ? ' ' : ''}${v.join(' ')}`).join('; ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchCspSrc(src, name, ...values) {
|
|
||||||
let def = src['default-src'];
|
|
||||||
let list = src[name];
|
|
||||||
if (def || list) {
|
|
||||||
if (!def) def = [];
|
|
||||||
if (!list) list = [...def];
|
|
||||||
if (values.includes('*')) list = src[name] = list.filter(v => !rxHOST.test(v));
|
|
||||||
list.push(...values.filter(v => !list.includes(v) && !def.includes(v)));
|
|
||||||
if (!list.length) delete src[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanUp(req) {
|
|
||||||
const key = req2key(req);
|
|
||||||
const data = stylesToPass[key];
|
|
||||||
if (data) {
|
|
||||||
delete stylesToPass[key];
|
|
||||||
clearTimeout(data.timer);
|
|
||||||
if (data.blobId) {
|
|
||||||
URL.revokeObjectURL(blobUrlPrefix + data.blobId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
|
|
||||||
function openNamedStyle(req) {
|
|
||||||
if (!req.url.includes('?')) { // skipping our usercss installer
|
|
||||||
chrome.tabs.update(req.tabId, {url: 'edit.html?id=' + req.url.split('#')[1]});
|
|
||||||
return {cancel: true};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function req2key(req) {
|
|
||||||
return req.tabId + ':' + req.frameId;
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,307 +0,0 @@
|
||||||
/* global API msg */// msg.js
|
|
||||||
/* global bgReady uuidIndex */// common.js
|
|
||||||
/* global chromeLocal chromeSync */// storage-util.js
|
|
||||||
/* global db */
|
|
||||||
/* global iconMan */
|
|
||||||
/* global prefs */
|
|
||||||
/* global styleUtil */
|
|
||||||
/* global tokenMan */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const syncMan = (() => {
|
|
||||||
//#region Init
|
|
||||||
|
|
||||||
const SYNC_DELAY = 1; // minutes
|
|
||||||
const SYNC_INTERVAL = 30; // minutes
|
|
||||||
const STATES = Object.freeze({
|
|
||||||
connected: 'connected',
|
|
||||||
connecting: 'connecting',
|
|
||||||
disconnected: 'disconnected',
|
|
||||||
disconnecting: 'disconnecting',
|
|
||||||
});
|
|
||||||
const STORAGE_KEY = 'sync/state/';
|
|
||||||
const NO_LOGIN = ['webdav'];
|
|
||||||
const status = /** @namespace SyncManager.Status */ {
|
|
||||||
STATES,
|
|
||||||
state: STATES.disconnected,
|
|
||||||
syncing: false,
|
|
||||||
progress: null,
|
|
||||||
currentDriveName: null,
|
|
||||||
errorMessage: null,
|
|
||||||
login: false,
|
|
||||||
};
|
|
||||||
const compareRevision = (rev1, rev2) => rev1 - rev2;
|
|
||||||
let lastError = null;
|
|
||||||
let ctrl;
|
|
||||||
let currentDrive;
|
|
||||||
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
|
||||||
let ready = bgReady.styles.then(() => {
|
|
||||||
ready = true;
|
|
||||||
prefs.subscribe('sync.enabled',
|
|
||||||
(_, val) => val === 'none'
|
|
||||||
? syncMan.stop()
|
|
||||||
: syncMan.start(val, true),
|
|
||||||
{runNow: true});
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener(async ({name}) => {
|
|
||||||
if (name === 'syncNow') {
|
|
||||||
await syncMan.syncNow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Exports
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
async delete(...args) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (!currentDrive) return;
|
|
||||||
schedule();
|
|
||||||
return ctrl.delete(...args);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {Promise<SyncManager.Status>} */
|
|
||||||
async getStatus() {
|
|
||||||
return status;
|
|
||||||
},
|
|
||||||
|
|
||||||
async login(name) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (!name) name = prefs.get('sync.enabled');
|
|
||||||
await tokenMan.revokeToken(name);
|
|
||||||
try {
|
|
||||||
await tokenMan.getToken(name, true);
|
|
||||||
status.login = true;
|
|
||||||
} catch (err) {
|
|
||||||
status.login = false;
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
emitStatusChange();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async putDoc({_id, _rev}) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (!currentDrive) return;
|
|
||||||
schedule();
|
|
||||||
return ctrl.put(_id, _rev);
|
|
||||||
},
|
|
||||||
|
|
||||||
async setDriveOptions(driveName, options) {
|
|
||||||
const key = `secure/sync/driveOptions/${driveName}`;
|
|
||||||
await chromeSync.setValue(key, options);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getDriveOptions(driveName) {
|
|
||||||
const key = `secure/sync/driveOptions/${driveName}`;
|
|
||||||
return await chromeSync.getValue(key) || {};
|
|
||||||
},
|
|
||||||
|
|
||||||
async start(name, fromPref = false) {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (!ctrl) await initController();
|
|
||||||
|
|
||||||
if (currentDrive) return;
|
|
||||||
currentDrive = await getDrive(name);
|
|
||||||
ctrl.use(currentDrive);
|
|
||||||
|
|
||||||
status.state = STATES.connecting;
|
|
||||||
status.currentDriveName = currentDrive.name;
|
|
||||||
emitStatusChange();
|
|
||||||
|
|
||||||
if (fromPref || NO_LOGIN.includes(currentDrive.name)) {
|
|
||||||
status.login = true;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await syncMan.login(name);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
status.errorMessage = err.message;
|
|
||||||
lastError = err;
|
|
||||||
emitStatusChange();
|
|
||||||
return syncMan.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctrl.init();
|
|
||||||
|
|
||||||
await syncMan.syncNow(name);
|
|
||||||
prefs.set('sync.enabled', name);
|
|
||||||
status.state = STATES.connected;
|
|
||||||
schedule(SYNC_INTERVAL);
|
|
||||||
emitStatusChange();
|
|
||||||
},
|
|
||||||
|
|
||||||
async stop() {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (!currentDrive) return;
|
|
||||||
chrome.alarms.clear('syncNow');
|
|
||||||
status.state = STATES.disconnecting;
|
|
||||||
emitStatusChange();
|
|
||||||
try {
|
|
||||||
await ctrl.uninit();
|
|
||||||
await tokenMan.revokeToken(currentDrive.name);
|
|
||||||
await chromeLocal.remove(STORAGE_KEY + currentDrive.name);
|
|
||||||
} catch (e) {}
|
|
||||||
currentDrive = null;
|
|
||||||
prefs.set('sync.enabled', 'none');
|
|
||||||
status.state = STATES.disconnected;
|
|
||||||
status.currentDriveName = null;
|
|
||||||
status.login = false;
|
|
||||||
emitStatusChange();
|
|
||||||
},
|
|
||||||
|
|
||||||
async syncNow() {
|
|
||||||
if (ready.then) await ready;
|
|
||||||
if (!currentDrive || !status.login) {
|
|
||||||
console.warn('cannot sync when disconnected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await ctrl.syncNow();
|
|
||||||
status.errorMessage = null;
|
|
||||||
lastError = null;
|
|
||||||
} catch (err) {
|
|
||||||
err.message = translateErrorMessage(err);
|
|
||||||
status.errorMessage = err.message;
|
|
||||||
lastError = err;
|
|
||||||
if (isGrantError(err)) {
|
|
||||||
status.login = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitStatusChange();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Utils
|
|
||||||
|
|
||||||
async function initController() {
|
|
||||||
await require(['/vendor/db-to-cloud/db-to-cloud']); /* global dbToCloud */
|
|
||||||
ctrl = dbToCloud.dbToCloud({
|
|
||||||
onGet: styleUtil.uuid2style,
|
|
||||||
async onPut(doc) {
|
|
||||||
const id = uuidIndex.get(doc._id);
|
|
||||||
const oldCust = uuidIndex.custom[id];
|
|
||||||
const oldDoc = oldCust || styleUtil.id2style(id);
|
|
||||||
const diff = oldDoc ? compareRevision(oldDoc._rev, doc._rev) : -1;
|
|
||||||
if (!diff) return;
|
|
||||||
if (diff > 0) {
|
|
||||||
syncMan.putDoc(oldDoc);
|
|
||||||
} else if (oldCust) {
|
|
||||||
uuidIndex.custom[id] = doc;
|
|
||||||
} else {
|
|
||||||
delete doc.id;
|
|
||||||
if (id) doc.id = id;
|
|
||||||
doc.id = await db.styles.put(doc);
|
|
||||||
await styleUtil.handleSave(doc, {reason: 'sync'});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDelete(_id, rev) {
|
|
||||||
const id = uuidIndex.get(_id);
|
|
||||||
const oldDoc = styleUtil.id2style(id);
|
|
||||||
return oldDoc &&
|
|
||||||
compareRevision(oldDoc._rev, rev) <= 0 &&
|
|
||||||
API.styles.delete(id, 'sync');
|
|
||||||
},
|
|
||||||
async onFirstSync() {
|
|
||||||
for (const i of Object.values(uuidIndex.custom).concat(await API.styles.getAll())) {
|
|
||||||
ctrl.put(i._id, i._rev);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onProgress(e) {
|
|
||||||
if (e.phase === 'start') {
|
|
||||||
status.syncing = true;
|
|
||||||
} else if (e.phase === 'end') {
|
|
||||||
status.syncing = false;
|
|
||||||
status.progress = null;
|
|
||||||
} else {
|
|
||||||
status.progress = e;
|
|
||||||
}
|
|
||||||
emitStatusChange();
|
|
||||||
},
|
|
||||||
compareRevision,
|
|
||||||
getState(drive) {
|
|
||||||
return chromeLocal.getValue(STORAGE_KEY + drive.name);
|
|
||||||
},
|
|
||||||
setState(drive, state) {
|
|
||||||
return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
|
|
||||||
},
|
|
||||||
retryMaxAttempts: 10,
|
|
||||||
retryExp: 1.2,
|
|
||||||
retryDelay: 6,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitStatusChange() {
|
|
||||||
msg.broadcastExtension({method: 'syncStatusUpdate', status});
|
|
||||||
iconMan.overrideBadge(getErrorBadge());
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNetworkError(err) {
|
|
||||||
return (
|
|
||||||
err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message) ||
|
|
||||||
err.code === 502
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGrantError(err) {
|
|
||||||
if (err.code === 401) return true;
|
|
||||||
if (err.code === 400 && /invalid_grant/.test(err.message)) return true;
|
|
||||||
if (err.name === 'TokenError') return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorBadge() {
|
|
||||||
if (status.state === STATES.connected &&
|
|
||||||
(!status.login || lastError && !isNetworkError(lastError))) {
|
|
||||||
return {
|
|
||||||
text: 'x',
|
|
||||||
color: '#F00',
|
|
||||||
title: !status.login ? 'syncErrorRelogin' : `${
|
|
||||||
chrome.i18n.getMessage('syncError')
|
|
||||||
}\n---------------------\n${
|
|
||||||
// splitting to limit each line length
|
|
||||||
lastError.message.replace(/.{60,}?\s(?=.{30,})/g, '$&\n')
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDrive(name) {
|
|
||||||
if (name === 'dropbox' || name === 'google' || name === 'onedrive' || name === 'webdav') {
|
|
||||||
const options = await syncMan.getDriveOptions(name);
|
|
||||||
options.getAccessToken = () => tokenMan.getToken(name);
|
|
||||||
options.fetch = name === 'webdav' ? fetchWebDAV.bind(options) : fetch;
|
|
||||||
return dbToCloud.drive[name](options);
|
|
||||||
}
|
|
||||||
throw new Error(`unknown cloud name: ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @this {Object} DriveOptions */
|
|
||||||
function fetchWebDAV(url, init = {}) {
|
|
||||||
init.credentials = 'omit'; // circumventing nextcloud CSRF token error
|
|
||||||
init.headers = Object.assign({}, init.headers, {
|
|
||||||
Authorization: `Basic ${btoa(`${this.username || ''}:${this.password || ''}`)}`,
|
|
||||||
});
|
|
||||||
return fetch(url, init);
|
|
||||||
}
|
|
||||||
|
|
||||||
function schedule(delay = SYNC_DELAY) {
|
|
||||||
chrome.alarms.create('syncNow', {
|
|
||||||
delayInMinutes: delay, // fractional values are supported
|
|
||||||
periodInMinutes: SYNC_INTERVAL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function translateErrorMessage(err) {
|
|
||||||
if (err.name === 'LockError') {
|
|
||||||
return browser.i18n.getMessage('syncErrorLock', new Date(err.expire).toLocaleString([], {timeStyle: 'short'}));
|
|
||||||
}
|
|
||||||
return err.message || String(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
})();
|
|
|
@ -1,62 +0,0 @@
|
||||||
/* global bgReady */// common.js
|
|
||||||
/* global navMan */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const tabMan = (() => {
|
|
||||||
const listeners = new Set();
|
|
||||||
const cache = new Map();
|
|
||||||
chrome.tabs.onRemoved.addListener(tabId => cache.delete(tabId));
|
|
||||||
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
|
|
||||||
|
|
||||||
bgReady.all.then(() => {
|
|
||||||
navMan.onUrlChange(({tabId, frameId, url}) => {
|
|
||||||
const oldUrl = !frameId && tabMan.get(tabId, 'url', frameId);
|
|
||||||
tabMan.set(tabId, 'url', frameId, url);
|
|
||||||
if (frameId) return;
|
|
||||||
for (const fn of listeners) {
|
|
||||||
try {
|
|
||||||
fn({tabId, url, oldUrl});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
onUpdate(fn) {
|
|
||||||
listeners.add(fn);
|
|
||||||
},
|
|
||||||
|
|
||||||
get(tabId, ...keys) {
|
|
||||||
return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* number of keys is arbitrary, last arg is value, `undefined` will delete the last key from meta
|
|
||||||
* (tabId, 'foo', 123) will set tabId's meta to {foo: 123},
|
|
||||||
* (tabId, 'foo', 'bar', 'etc', 123) will set tabId's meta to {foo: {bar: {etc: 123}}}
|
|
||||||
*/
|
|
||||||
set(tabId, ...args) {
|
|
||||||
let meta = cache.get(tabId);
|
|
||||||
if (!meta) {
|
|
||||||
meta = {};
|
|
||||||
cache.set(tabId, meta);
|
|
||||||
}
|
|
||||||
const value = args.pop();
|
|
||||||
const lastKey = args.pop();
|
|
||||||
for (const key of args) meta = meta[key] || (meta[key] = {});
|
|
||||||
if (value === undefined) {
|
|
||||||
delete meta[lastKey];
|
|
||||||
} else {
|
|
||||||
meta[lastKey] = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/** @returns {IterableIterator<number>} */
|
|
||||||
list() {
|
|
||||||
return cache.keys();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
|
@ -1,270 +0,0 @@
|
||||||
/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js
|
|
||||||
/* global chromeLocal */// storage-util.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* exported tokenMan */
|
|
||||||
const tokenMan = (() => {
|
|
||||||
const AUTH = {
|
|
||||||
dropbox: {
|
|
||||||
flow: 'token',
|
|
||||||
clientId: 'zg52vphuapvpng9',
|
|
||||||
authURL: 'https://www.dropbox.com/oauth2/authorize',
|
|
||||||
tokenURL: 'https://api.dropboxapi.com/oauth2/token',
|
|
||||||
revoke: token =>
|
|
||||||
fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
google: {
|
|
||||||
flow: 'code',
|
|
||||||
clientId: '283762574871-d4u58s4arra5jdan2gr00heasjlttt1e.apps.googleusercontent.com',
|
|
||||||
clientSecret: 'J0nc5TlR_0V_ex9-sZk-5faf',
|
|
||||||
authURL: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
||||||
authQuery: {
|
|
||||||
// NOTE: Google needs 'prompt' parameter to deliver multiple refresh
|
|
||||||
// tokens for multiple machines.
|
|
||||||
// https://stackoverflow.com/q/18519185
|
|
||||||
access_type: 'offline',
|
|
||||||
prompt: 'consent',
|
|
||||||
},
|
|
||||||
tokenURL: 'https://oauth2.googleapis.com/token',
|
|
||||||
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
|
|
||||||
// FIXME: https://github.com/openstyles/stylus/issues/1248
|
|
||||||
// revoke: token => {
|
|
||||||
// const params = {token};
|
|
||||||
// return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
onedrive: {
|
|
||||||
flow: 'code',
|
|
||||||
clientId: '3864ce03-867c-4ad8-9856-371a097d47b1',
|
|
||||||
clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
|
|
||||||
authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
|
||||||
tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
||||||
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
|
|
||||||
},
|
|
||||||
userstylesworld: {
|
|
||||||
flow: 'code',
|
|
||||||
clientId: 'zeDmKhJIfJqULtcrGMsWaxRtWHEimKgS',
|
|
||||||
clientSecret: 'wqHsvTuThQmXmDiVvOpZxPwSIbyycNFImpAOTxjaIRqDbsXcTOqrymMJKsOMuibFaij' +
|
|
||||||
'ZZAkVYTDbLkQuYFKqgpMsMlFlgwQOYHvHFbgxQHDTwwdOroYhOwFuekCwXUlk',
|
|
||||||
authURL: URLS.usw + 'api/oauth/style/link',
|
|
||||||
tokenURL: URLS.usw + 'api/oauth/token',
|
|
||||||
redirect_uri: 'https://gusted.xyz/callback_helper/',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const NETWORK_LATENCY = 30; // seconds
|
|
||||||
const DEFAULT_REDIRECT_URI = 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/';
|
|
||||||
|
|
||||||
let alwaysUseTab = !chrome.windows || (FIREFOX ? false : null);
|
|
||||||
|
|
||||||
class TokenError extends Error {
|
|
||||||
constructor(provider, message) {
|
|
||||||
super(`[${provider}] ${message}`);
|
|
||||||
this.name = 'TokenError';
|
|
||||||
this.provider = provider;
|
|
||||||
if (Error.captureStackTrace) {
|
|
||||||
Error.captureStackTrace(this, TokenError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
buildKeys(name, hooks) {
|
|
||||||
const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
|
|
||||||
const k = {
|
|
||||||
TOKEN: `${prefix}token`,
|
|
||||||
EXPIRE: `${prefix}expire`,
|
|
||||||
REFRESH: `${prefix}refresh`,
|
|
||||||
};
|
|
||||||
k.LIST = Object.values(k);
|
|
||||||
return k;
|
|
||||||
},
|
|
||||||
|
|
||||||
getClientId(name) {
|
|
||||||
return AUTH[name].clientId;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getToken(name, interactive, hooks) {
|
|
||||||
const k = tokenMan.buildKeys(name, hooks);
|
|
||||||
const obj = await chromeLocal.get(k.LIST);
|
|
||||||
if (obj[k.TOKEN]) {
|
|
||||||
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
|
|
||||||
return obj[k.TOKEN];
|
|
||||||
}
|
|
||||||
if (obj[k.REFRESH]) {
|
|
||||||
return refreshToken(name, k, obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!interactive) {
|
|
||||||
throw new TokenError(name, 'Token is missing');
|
|
||||||
}
|
|
||||||
return authUser(k, name, interactive, hooks);
|
|
||||||
},
|
|
||||||
|
|
||||||
async revokeToken(name, hooks) {
|
|
||||||
const provider = AUTH[name];
|
|
||||||
const k = tokenMan.buildKeys(name, hooks);
|
|
||||||
if (provider.revoke) {
|
|
||||||
try {
|
|
||||||
const token = await chromeLocal.getValue(k.TOKEN);
|
|
||||||
if (token) await provider.revoke(token);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await chromeLocal.remove(k.LIST);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function refreshToken(name, k, obj) {
|
|
||||||
if (!obj[k.REFRESH]) {
|
|
||||||
throw new TokenError(name, 'No refresh token');
|
|
||||||
}
|
|
||||||
const provider = AUTH[name];
|
|
||||||
const body = {
|
|
||||||
client_id: provider.clientId,
|
|
||||||
refresh_token: obj[k.REFRESH],
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
scope: provider.scopes.join(' '),
|
|
||||||
};
|
|
||||||
if (provider.clientSecret) {
|
|
||||||
body.client_secret = provider.clientSecret;
|
|
||||||
}
|
|
||||||
const result = await postQuery(provider.tokenURL, body);
|
|
||||||
if (!result.refresh_token) {
|
|
||||||
// reuse old refresh token
|
|
||||||
result.refresh_token = obj[k.REFRESH];
|
|
||||||
}
|
|
||||||
return handleTokenResult(result, k);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authUser(keys, name, interactive = false, hooks = null) {
|
|
||||||
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow']);
|
|
||||||
/* global webextLaunchWebAuthFlow */
|
|
||||||
const provider = AUTH[name];
|
|
||||||
const state = Math.random().toFixed(8).slice(2);
|
|
||||||
const query = {
|
|
||||||
response_type: provider.flow,
|
|
||||||
client_id: provider.clientId,
|
|
||||||
redirect_uri: provider.redirect_uri || DEFAULT_REDIRECT_URI,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
if (provider.scopes) {
|
|
||||||
query.scope = provider.scopes.join(' ');
|
|
||||||
}
|
|
||||||
if (provider.authQuery) {
|
|
||||||
Object.assign(query, provider.authQuery);
|
|
||||||
}
|
|
||||||
if (alwaysUseTab == null) {
|
|
||||||
alwaysUseTab = await detectVivaldiWebRequestBug();
|
|
||||||
}
|
|
||||||
if (hooks) hooks.query(query);
|
|
||||||
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
|
|
||||||
const width = Math.min(screen.availWidth - 100, 800);
|
|
||||||
const height = Math.min(screen.availHeight - 100, 800);
|
|
||||||
const wnd = !alwaysUseTab && await browser.windows.getLastFocused();
|
|
||||||
const finalUrl = await webextLaunchWebAuthFlow({
|
|
||||||
url,
|
|
||||||
alwaysUseTab,
|
|
||||||
interactive,
|
|
||||||
redirect_uri: query.redirect_uri,
|
|
||||||
windowOptions: wnd && Object.assign({
|
|
||||||
state: 'normal',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}, wnd.state !== 'minimized' && {
|
|
||||||
// Center the popup to the current window
|
|
||||||
top: Math.ceil(wnd.top + (wnd.height - width) / 2),
|
|
||||||
left: Math.ceil(wnd.left + (wnd.width - width) / 2),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const params = new URLSearchParams(
|
|
||||||
provider.flow === 'token' ?
|
|
||||||
new URL(finalUrl).hash.slice(1) :
|
|
||||||
new URL(finalUrl).search.slice(1)
|
|
||||||
);
|
|
||||||
if (params.get('state') !== state) {
|
|
||||||
throw new TokenError(name, `Unexpected state: ${params.get('state')}, expected: ${state}`);
|
|
||||||
}
|
|
||||||
let result;
|
|
||||||
if (provider.flow === 'token') {
|
|
||||||
const obj = {};
|
|
||||||
for (const [key, value] of params) {
|
|
||||||
obj[key] = value;
|
|
||||||
}
|
|
||||||
result = obj;
|
|
||||||
} else {
|
|
||||||
const code = params.get('code');
|
|
||||||
const body = {
|
|
||||||
code,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
client_id: provider.clientId,
|
|
||||||
redirect_uri: query.redirect_uri,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
if (provider.clientSecret) {
|
|
||||||
body.client_secret = provider.clientSecret;
|
|
||||||
}
|
|
||||||
result = await postQuery(provider.tokenURL, body);
|
|
||||||
}
|
|
||||||
return handleTokenResult(result, keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleTokenResult(result, k) {
|
|
||||||
await chromeLocal.set({
|
|
||||||
[k.TOKEN]: result.access_token,
|
|
||||||
[k.EXPIRE]: result.expires_in
|
|
||||||
? Date.now() + (result.expires_in - NETWORK_LATENCY) * 1000
|
|
||||||
: undefined,
|
|
||||||
[k.REFRESH]: result.refresh_token,
|
|
||||||
});
|
|
||||||
return result.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postQuery(url, body) {
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: body ? new URLSearchParams(body) : null,
|
|
||||||
};
|
|
||||||
const r = await fetch(url, options);
|
|
||||||
if (r.ok) {
|
|
||||||
return r.json();
|
|
||||||
}
|
|
||||||
const text = await r.text();
|
|
||||||
const err = new Error(`Failed to fetch (${r.status}): ${text}`);
|
|
||||||
err.code = r.status;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function detectVivaldiWebRequestBug() {
|
|
||||||
// Workaround for https://github.com/openstyles/stylus/issues/1182
|
|
||||||
// Note that modern Vivaldi isn't exposed in `navigator.userAgent` but it adds `extData` to tabs
|
|
||||||
const anyTab = await getActiveTab() || (await browser.tabs.query({}))[0];
|
|
||||||
if (anyTab && !(anyTab.extData || anyTab.vivExtData)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let bugged = true;
|
|
||||||
const TEST_URL = chrome.runtime.getURL('manifest.json');
|
|
||||||
const check = ({url}) => {
|
|
||||||
bugged = url !== TEST_URL;
|
|
||||||
};
|
|
||||||
chrome.webRequest.onBeforeRequest.addListener(check, {urls: [TEST_URL], types: ['main_frame']});
|
|
||||||
const {tabs: [tab]} = await browser.windows.create({
|
|
||||||
type: 'popup',
|
|
||||||
state: 'minimized',
|
|
||||||
url: TEST_URL,
|
|
||||||
});
|
|
||||||
await waitForTabUrl(tab);
|
|
||||||
chrome.windows.remove(tab.windowId);
|
|
||||||
chrome.webRequest.onBeforeRequest.removeListener(check);
|
|
||||||
return bugged;
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,348 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
/* global RX_META URLS debounce deepMerge download ignoreChromeError */// toolbox.js
|
|
||||||
/* global calcStyleDigest styleSectionsEqual */ // sections-util.js
|
|
||||||
/* global chromeLocal */// storage-util.js
|
|
||||||
/* global compareVersion */// cmpver.js
|
|
||||||
/* global db */
|
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* exported updateMan */
|
|
||||||
const updateMan = (() => {
|
|
||||||
const STATES = /** @namespace UpdaterStates */ {
|
|
||||||
UPDATED: 'updated',
|
|
||||||
SKIPPED: 'skipped',
|
|
||||||
UNREACHABLE: 'server unreachable',
|
|
||||||
// details for SKIPPED status
|
|
||||||
EDITED: 'locally edited',
|
|
||||||
MAYBE_EDITED: 'may be locally edited',
|
|
||||||
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
|
||||||
SAME_CODE: 'up-to-date: code sections are unchanged',
|
|
||||||
SAME_VERSION: 'up-to-date: version is unchanged',
|
|
||||||
ERROR_MD5: 'error: MD5 is invalid',
|
|
||||||
ERROR_JSON: 'error: JSON is invalid',
|
|
||||||
ERROR_VERSION: 'error: version is older than installed style',
|
|
||||||
};
|
|
||||||
const USO_STYLES_API = `${URLS.uso}api/v1/styles/`;
|
|
||||||
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
|
|
||||||
const RX_DATE2VER = new RegExp([
|
|
||||||
/^(\d{4})/,
|
|
||||||
/(0[1-9]|1(?:0|[12](?=\d\d))?|[2-9])/, // in ambiguous cases like yyyy123 the month will be 1
|
|
||||||
/(0[1-9]|[1-2][0-9]?|3[0-1]?|[4-9])/,
|
|
||||||
/\.([01][0-9]?|2[0-3]?|[3-9])/,
|
|
||||||
/\.([0-5][0-9]?|[6-9])$/,
|
|
||||||
].map(rx => rx.source).join(''));
|
|
||||||
const ALARM_NAME = 'scheduledUpdate';
|
|
||||||
const MIN_INTERVAL_MS = 60e3;
|
|
||||||
const RETRY_ERRORS = [
|
|
||||||
503, // service unavailable
|
|
||||||
429, // too many requests
|
|
||||||
];
|
|
||||||
let usoReferers = 0;
|
|
||||||
let lastUpdateTime;
|
|
||||||
let checkingAll = false;
|
|
||||||
let logQueue = [];
|
|
||||||
let logLastWriteTime = 0;
|
|
||||||
|
|
||||||
chromeLocal.getValue('lastUpdateTime').then(val => {
|
|
||||||
lastUpdateTime = val || Date.now();
|
|
||||||
prefs.subscribe('updateInterval', schedule, {runNow: true});
|
|
||||||
chrome.alarms.onAlarm.addListener(onAlarm);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
checkAllStyles,
|
|
||||||
checkStyle,
|
|
||||||
getStates: () => STATES,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function checkAllStyles({
|
|
||||||
save = true,
|
|
||||||
ignoreDigest,
|
|
||||||
observe,
|
|
||||||
} = {}) {
|
|
||||||
resetInterval();
|
|
||||||
checkingAll = true;
|
|
||||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
|
||||||
const styles = (await API.styles.getAll())
|
|
||||||
.filter(style => style.updateUrl && style.updatable !== false);
|
|
||||||
if (port) port.postMessage({count: styles.length});
|
|
||||||
log('');
|
|
||||||
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
|
||||||
await Promise.all(
|
|
||||||
styles.map(style =>
|
|
||||||
checkStyle({style, port, save, ignoreDigest})));
|
|
||||||
if (port) port.postMessage({done: true});
|
|
||||||
if (port) port.disconnect();
|
|
||||||
log('');
|
|
||||||
checkingAll = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
id?: number,
|
|
||||||
style?: StyleObj,
|
|
||||||
port?: chrome.runtime.Port,
|
|
||||||
save?: boolean,
|
|
||||||
ignoreDigest?: boolean,
|
|
||||||
}} opts
|
|
||||||
* @returns {{
|
|
||||||
style: StyleObj,
|
|
||||||
updated?: boolean,
|
|
||||||
error?: any,
|
|
||||||
STATES: UpdaterStates,
|
|
||||||
}}
|
|
||||||
|
|
||||||
Original style digests are calculated in these cases:
|
|
||||||
* style is installed or updated from server
|
|
||||||
* non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
|
|
||||||
|
|
||||||
Update check proceeds in these cases:
|
|
||||||
* style has the original digest and it's equal to the current digest
|
|
||||||
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
|
|
||||||
* [ignoreDigest: none/false] style doesn't yet have the original digest
|
|
||||||
so we compare the code to the server code and if it's the same we save the digest,
|
|
||||||
otherwise we skip the style and report MAYBE_EDITED status
|
|
||||||
|
|
||||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
|
||||||
*/
|
|
||||||
async function checkStyle(opts) {
|
|
||||||
let {id} = opts;
|
|
||||||
const {
|
|
||||||
style = await API.styles.get(id),
|
|
||||||
ignoreDigest,
|
|
||||||
port,
|
|
||||||
save,
|
|
||||||
} = opts;
|
|
||||||
if (!id) id = style.id;
|
|
||||||
const {md5Url} = style;
|
|
||||||
let {usercssData: ucd, updateUrl} = style;
|
|
||||||
let res, state;
|
|
||||||
try {
|
|
||||||
await checkIfEdited();
|
|
||||||
res = {
|
|
||||||
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
|
|
||||||
updated: true,
|
|
||||||
};
|
|
||||||
state = STATES.UPDATED;
|
|
||||||
} catch (err) {
|
|
||||||
const error = err === 0 && STATES.UNREACHABLE ||
|
|
||||||
err && err.message ||
|
|
||||||
err;
|
|
||||||
res = {error, style, STATES};
|
|
||||||
state = `${STATES.SKIPPED} (${Array.isArray(err) ? err[0].message : error})`;
|
|
||||||
}
|
|
||||||
log(`${state} #${id} ${style.customName || style.name}`);
|
|
||||||
if (port) port.postMessage(res);
|
|
||||||
return res;
|
|
||||||
|
|
||||||
async function checkIfEdited() {
|
|
||||||
if (!ignoreDigest &&
|
|
||||||
style.originalDigest &&
|
|
||||||
style.originalDigest !== await calcStyleDigest(style)) {
|
|
||||||
return Promise.reject(STATES.EDITED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateUSO() {
|
|
||||||
const md5 = await tryDownload(md5Url);
|
|
||||||
if (!md5 || md5.length !== 32) {
|
|
||||||
return Promise.reject(STATES.ERROR_MD5);
|
|
||||||
}
|
|
||||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
|
||||||
return Promise.reject(STATES.SAME_MD5);
|
|
||||||
}
|
|
||||||
let varsUrl = '';
|
|
||||||
if (!ucd) {
|
|
||||||
ucd = {};
|
|
||||||
varsUrl = updateUrl;
|
|
||||||
updateUrl = style.updateUrl = `${USO_STYLES_API}${md5Url.match(/\/(\d+)/)[1]}`;
|
|
||||||
}
|
|
||||||
usoSpooferStart();
|
|
||||||
let json;
|
|
||||||
try {
|
|
||||||
json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
|
||||||
json = await updateUsercss(json.css) ||
|
|
||||||
(await API.uso.toUsercss(json)).style;
|
|
||||||
if (varsUrl) await API.uso.useVarsUrl(json, varsUrl);
|
|
||||||
} finally {
|
|
||||||
usoSpooferStop();
|
|
||||||
}
|
|
||||||
// USO may not provide a correctly updated originalMd5 (#555)
|
|
||||||
json.originalMd5 = md5;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateUsercss(css) {
|
|
||||||
let oldVer = ucd.version;
|
|
||||||
let {etag: oldEtag, updateUrl} = style;
|
|
||||||
const m2 = (css || URLS.extractUsoArchiveId(updateUrl)) &&
|
|
||||||
await getUsoEmbeddedMeta(css);
|
|
||||||
if (m2 && m2.updateUrl) {
|
|
||||||
updateUrl = m2.updateUrl;
|
|
||||||
oldVer = m2.usercssData.version || '0';
|
|
||||||
oldEtag = '';
|
|
||||||
} else if (css) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (oldEtag && oldEtag === await downloadEtag()) {
|
|
||||||
return Promise.reject(STATES.SAME_CODE);
|
|
||||||
}
|
|
||||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
|
||||||
const {headers: {etag}, response} = await tryDownload(updateUrl, RH_ETAG);
|
|
||||||
const json = await API.usercss.buildMeta({sourceCode: response, etag, updateUrl});
|
|
||||||
const delta = compareVersion(json.usercssData.version, oldVer);
|
|
||||||
let err;
|
|
||||||
if (!delta && !ignoreDigest) {
|
|
||||||
// re-install is invalid in a soft upgrade
|
|
||||||
err = response === style.sourceCode
|
|
||||||
? STATES.SAME_CODE
|
|
||||||
: !URLS.isLocalhost(updateUrl) && STATES.SAME_VERSION;
|
|
||||||
}
|
|
||||||
if (delta < 0) {
|
|
||||||
// downgrade is always invalid
|
|
||||||
err = STATES.ERROR_VERSION;
|
|
||||||
}
|
|
||||||
if (err && etag && !style.etag) {
|
|
||||||
// first check of ETAG, gonna write it directly to DB as it's too trivial to sync or announce
|
|
||||||
style.etag = etag;
|
|
||||||
await db.styles.put(style);
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
? Promise.reject(err)
|
|
||||||
: API.usercss.buildCode(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function maybeSave(json) {
|
|
||||||
json.id = id;
|
|
||||||
// keep current state
|
|
||||||
delete json.customName;
|
|
||||||
delete json.enabled;
|
|
||||||
const newStyle = Object.assign({}, style, json);
|
|
||||||
newStyle.updateDate = getDateFromVer(newStyle) || Date.now();
|
|
||||||
// update digest even if save === false as there might be just a space added etc.
|
|
||||||
if (!ucd && styleSectionsEqual(json, style)) {
|
|
||||||
style.originalDigest = (await API.styles.install(newStyle)).originalDigest;
|
|
||||||
return Promise.reject(STATES.SAME_CODE);
|
|
||||||
}
|
|
||||||
if (!style.originalDigest && !ignoreDigest) {
|
|
||||||
return Promise.reject(STATES.MAYBE_EDITED);
|
|
||||||
}
|
|
||||||
return !save ? newStyle :
|
|
||||||
(ucd ? API.usercss.install : API.styles.install)(newStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryDownload(url, params) {
|
|
||||||
let {retryDelay = 1000} = opts;
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
params = deepMerge(params || {}, {headers: {'Cache-Control': 'no-cache'}});
|
|
||||||
return await download(url, params);
|
|
||||||
} catch (code) {
|
|
||||||
if (!RETRY_ERRORS.includes(code) ||
|
|
||||||
retryDelay > MIN_INTERVAL_MS) {
|
|
||||||
return Promise.reject(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
retryDelay *= 1.25;
|
|
||||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadEtag() {
|
|
||||||
const opts = Object.assign({method: 'head'}, RH_ETAG);
|
|
||||||
const req = await tryDownload(style.updateUrl, opts);
|
|
||||||
return req.headers.etag;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDateFromVer(style) {
|
|
||||||
const m = RX_DATE2VER.exec((style.usercssData || {}).version);
|
|
||||||
if (m) {
|
|
||||||
m[2]--; // month is 0-based in `Date` constructor
|
|
||||||
return new Date(...m.slice(1)).getTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
|
|
||||||
function getUsoEmbeddedMeta(code = style.sourceCode) {
|
|
||||||
const isRaw = arguments[0];
|
|
||||||
const m = code.includes('@updateURL') && (isRaw ? code : code.replace(RX_META, '')).match(RX_META);
|
|
||||||
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function schedule() {
|
|
||||||
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
|
|
||||||
if (interval > 0) {
|
|
||||||
const elapsed = Math.max(0, Date.now() - lastUpdateTime);
|
|
||||||
chrome.alarms.create(ALARM_NAME, {
|
|
||||||
when: Date.now() + Math.max(MIN_INTERVAL_MS, interval - elapsed),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
chrome.alarms.clear(ALARM_NAME, ignoreChromeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAlarm({name}) {
|
|
||||||
if (name === ALARM_NAME) checkAllStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetInterval() {
|
|
||||||
chromeLocal.setValue('lastUpdateTime', lastUpdateTime = Date.now());
|
|
||||||
schedule();
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(text) {
|
|
||||||
logQueue.push({text, time: new Date().toLocaleString()});
|
|
||||||
debounce(flushQueue, text && checkingAll ? 1000 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function flushQueue(lines) {
|
|
||||||
if (!lines) {
|
|
||||||
flushQueue(await chromeLocal.getValue('updateLog') || []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const time = Date.now() - logLastWriteTime > 11e3 ?
|
|
||||||
logQueue[0].time + ' ' :
|
|
||||||
'';
|
|
||||||
if (logQueue[0] && !logQueue[0].text) {
|
|
||||||
logQueue.shift();
|
|
||||||
if (lines[lines.length - 1]) lines.push('');
|
|
||||||
}
|
|
||||||
lines.splice(0, lines.length - 1000);
|
|
||||||
lines.push(time + (logQueue[0] && logQueue[0].text || ''));
|
|
||||||
lines.push(...logQueue.slice(1).map(item => item.text));
|
|
||||||
|
|
||||||
chromeLocal.setValue('updateLog', lines);
|
|
||||||
logLastWriteTime = Date.now();
|
|
||||||
logQueue = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function usoSpooferStart() {
|
|
||||||
if (++usoReferers === 1) {
|
|
||||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
|
||||||
usoSpoofer,
|
|
||||||
{types: ['xmlhttprequest'], urls: [USO_STYLES_API + '*']},
|
|
||||||
['blocking', 'requestHeaders', chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS]
|
|
||||||
.filter(Boolean));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function usoSpooferStop() {
|
|
||||||
if (--usoReferers <= 0) {
|
|
||||||
usoReferers = 0;
|
|
||||||
chrome.webRequest.onBeforeSendHeaders.removeListener(usoSpoofer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebResponseHeadersDetails | browser.webRequest._OnBeforeSendHeadersDetails} info */
|
|
||||||
function usoSpoofer(info) {
|
|
||||||
if (info.tabId < 0 && URLS.ownOrigin.startsWith(info.initiator || info.originUrl || '')) {
|
|
||||||
const {requestHeaders: hh} = info;
|
|
||||||
const i = (hh.findIndex(h => /^referer$/i.test(h.name)) + 1 || hh.push({})) - 1;
|
|
||||||
hh[i].name = 'referer';
|
|
||||||
hh[i].value = URLS.uso;
|
|
||||||
return {requestHeaders: hh};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
199
background/update.js
Normal file
199
background/update.js
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||||
|
/* global calcStyleDigest */
|
||||||
|
/* global usercss semverCompare usercssHelper */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var updater = {
|
||||||
|
|
||||||
|
COUNT: 'count',
|
||||||
|
UPDATED: 'updated',
|
||||||
|
SKIPPED: 'skipped',
|
||||||
|
DONE: 'done',
|
||||||
|
|
||||||
|
// details for SKIPPED status
|
||||||
|
EDITED: 'locally edited',
|
||||||
|
MAYBE_EDITED: 'may be locally edited',
|
||||||
|
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||||
|
SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||||
|
SAME_VERSION: 'up-to-date: version is unchanged',
|
||||||
|
ERROR_MD5: 'error: MD5 is invalid',
|
||||||
|
ERROR_JSON: 'error: JSON is invalid',
|
||||||
|
ERROR_VERSION: 'error: version is older than installed style',
|
||||||
|
|
||||||
|
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
||||||
|
|
||||||
|
checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
|
||||||
|
updater.resetInterval();
|
||||||
|
updater.checkAllStyles.running = true;
|
||||||
|
return getStyles({}).then(styles => {
|
||||||
|
styles = styles.filter(style => style.updateUrl);
|
||||||
|
observer(updater.COUNT, styles.length);
|
||||||
|
updater.log('');
|
||||||
|
updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
||||||
|
return Promise.all(
|
||||||
|
styles.map(style =>
|
||||||
|
updater.checkStyle({style, observer, save, ignoreDigest})));
|
||||||
|
}).then(() => {
|
||||||
|
observer(updater.DONE);
|
||||||
|
updater.log('');
|
||||||
|
updater.checkAllStyles.running = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
|
||||||
|
/*
|
||||||
|
Original style digests are calculated in these cases:
|
||||||
|
* style is installed or updated from server
|
||||||
|
* style is checked for an update and its code is equal to the server code
|
||||||
|
|
||||||
|
Update check proceeds in these cases:
|
||||||
|
* style has the original digest and it's equal to the current digest
|
||||||
|
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
|
||||||
|
* [ignoreDigest: none/false] style doesn't yet have the original digest
|
||||||
|
so we compare the code to the server code and if it's the same we save the digest,
|
||||||
|
otherwise we skip the style and report MAYBE_EDITED status
|
||||||
|
|
||||||
|
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||||
|
*/
|
||||||
|
return Promise.resolve(style)
|
||||||
|
.then([calcStyleDigest][!ignoreDigest ? 0 : 'skip'])
|
||||||
|
.then([checkIfEdited][!ignoreDigest ? 0 : 'skip'])
|
||||||
|
.then([maybeUpdateUSO, maybeUpdateUsercss][style.usercssData ? 1 : 0])
|
||||||
|
.then(maybeSave)
|
||||||
|
.then(reportSuccess)
|
||||||
|
.catch(reportFailure);
|
||||||
|
|
||||||
|
function reportSuccess(saved) {
|
||||||
|
observer(updater.UPDATED, saved);
|
||||||
|
updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportFailure(err) {
|
||||||
|
observer(updater.SKIPPED, style, err);
|
||||||
|
err = err === 0 ? 'server unreachable' : err;
|
||||||
|
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfEdited(digest) {
|
||||||
|
if (style.originalDigest && style.originalDigest !== digest) {
|
||||||
|
return Promise.reject(updater.EDITED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeUpdateUSO() {
|
||||||
|
return download(style.md5Url).then(md5 => {
|
||||||
|
if (!md5 || md5.length !== 32) {
|
||||||
|
return Promise.reject(updater.ERROR_MD5);
|
||||||
|
}
|
||||||
|
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||||
|
return Promise.reject(updater.SAME_MD5);
|
||||||
|
}
|
||||||
|
return download(style.updateUrl)
|
||||||
|
.then(text => tryJSONparse(text));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeUpdateUsercss() {
|
||||||
|
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||||
|
return download(style.updateUrl).then(text => {
|
||||||
|
const json = usercss.buildMeta(text);
|
||||||
|
const {usercssData: {version}} = style;
|
||||||
|
const {usercssData: {version: newVersion}} = json;
|
||||||
|
switch (Math.sign(semverCompare(version, newVersion))) {
|
||||||
|
case 0:
|
||||||
|
// re-install is invalid in a soft upgrade
|
||||||
|
if (!ignoreDigest) {
|
||||||
|
return Promise.reject(updater.SAME_VERSION);
|
||||||
|
} else if (text === style.sourceCode) {
|
||||||
|
return Promise.reject(updater.SAME_CODE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// downgrade is always invalid
|
||||||
|
return Promise.reject(updater.ERROR_VERSION);
|
||||||
|
}
|
||||||
|
return usercss.buildCode(json);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeSave(json = {}) {
|
||||||
|
// usercss is already validated while building
|
||||||
|
if (!json.usercssData && !styleJSONseemsValid(json)) {
|
||||||
|
return Promise.reject(updater.ERROR_JSON);
|
||||||
|
}
|
||||||
|
json.id = style.id;
|
||||||
|
json.updateDate = Date.now();
|
||||||
|
json.reason = 'update';
|
||||||
|
// keep current state
|
||||||
|
delete json.enabled;
|
||||||
|
// keep local name customizations
|
||||||
|
delete json.name;
|
||||||
|
|
||||||
|
if (styleSectionsEqual(json, style)) {
|
||||||
|
// update digest even if save === false as there might be just a space added etc.
|
||||||
|
saveStyle(Object.assign(json, {reason: 'update-digest'}));
|
||||||
|
return Promise.reject(updater.SAME_CODE);
|
||||||
|
} else if (!style.originalDigest && !ignoreDigest) {
|
||||||
|
return Promise.reject(updater.MAYBE_EDITED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !save ? json :
|
||||||
|
json.usercssData
|
||||||
|
? usercssHelper.save(json)
|
||||||
|
: saveStyle(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleJSONseemsValid(json) {
|
||||||
|
return json
|
||||||
|
&& json.sections
|
||||||
|
&& json.sections.length
|
||||||
|
&& typeof json.sections.every === 'function'
|
||||||
|
&& typeof json.sections[0].code === 'string';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
schedule() {
|
||||||
|
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
|
||||||
|
if (interval) {
|
||||||
|
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
|
||||||
|
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
|
||||||
|
} else {
|
||||||
|
debounce.unregister(updater.checkAllStyles);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetInterval() {
|
||||||
|
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
|
||||||
|
updater.schedule();
|
||||||
|
},
|
||||||
|
|
||||||
|
log: (() => {
|
||||||
|
let queue = [];
|
||||||
|
let lastWriteTime = 0;
|
||||||
|
return text => {
|
||||||
|
queue.push({text, time: new Date().toLocaleString()});
|
||||||
|
debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0);
|
||||||
|
};
|
||||||
|
function flushQueue() {
|
||||||
|
chromeLocal.getValue('updateLog').then((lines = []) => {
|
||||||
|
const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
|
||||||
|
if (!queue[0].text) {
|
||||||
|
queue.shift();
|
||||||
|
if (lines[lines.length - 1]) {
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.splice(0, lines.length - 1000);
|
||||||
|
lines.push(time + queue[0].text);
|
||||||
|
lines.push(...queue.slice(1).map(item => item.text));
|
||||||
|
chromeLocal.setValue('updateLog', lines);
|
||||||
|
lastWriteTime = Date.now();
|
||||||
|
queue = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
|
||||||
|
updater.schedule();
|
||||||
|
prefs.subscribe(['updateInterval'], updater.schedule);
|
107
background/usercss-helper.js
Normal file
107
background/usercss-helper.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/* global usercss saveStyle getStyles chromeLocal */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var usercssHelper = (() => {
|
||||||
|
function buildMeta(style) {
|
||||||
|
if (style.usercssData) {
|
||||||
|
return Promise.resolve(style);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {sourceCode} = style;
|
||||||
|
// allow sourceCode to be normalized
|
||||||
|
delete style.sourceCode;
|
||||||
|
return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style));
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCode(style) {
|
||||||
|
return usercss.buildCode(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapReject(pending) {
|
||||||
|
return pending
|
||||||
|
.catch(err => new Error(Array.isArray(err) ? err.join('\n') : err.message || String(err)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the source and find the duplication
|
||||||
|
function build({sourceCode, checkDup = false}, noReject) {
|
||||||
|
const pending = buildMeta({sourceCode})
|
||||||
|
.then(style => Promise.all([
|
||||||
|
buildCode(style),
|
||||||
|
checkDup && findDup(style)
|
||||||
|
]))
|
||||||
|
.then(([style, dup]) => ({style, dup}));
|
||||||
|
|
||||||
|
return noReject ? wrapReject(pending) : pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(style, noReject) {
|
||||||
|
const pending = buildMeta(style)
|
||||||
|
.then(assignVars)
|
||||||
|
.then(buildCode)
|
||||||
|
.then(saveStyle);
|
||||||
|
|
||||||
|
return noReject ? wrapReject(pending) : pending;
|
||||||
|
|
||||||
|
function assignVars(style) {
|
||||||
|
if (style.reason === 'config' && style.id) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
return findDup(style).then(dup => {
|
||||||
|
if (dup) {
|
||||||
|
style.id = dup.id;
|
||||||
|
if (style.reason !== 'config') {
|
||||||
|
// preserve style.vars during update
|
||||||
|
usercss.assignVars(style, dup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDup(style) {
|
||||||
|
if (style.id) {
|
||||||
|
return getStyles({id: style.id}).then(s => s[0]);
|
||||||
|
}
|
||||||
|
return getStyles().then(styles =>
|
||||||
|
styles.find(target => {
|
||||||
|
if (!target.usercssData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return target.usercssData.name === style.usercssData.name &&
|
||||||
|
target.usercssData.namespace === style.usercssData.namespace;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInstallPage(tab, {url = tab.url, direct, downloaded} = {}) {
|
||||||
|
if (direct && !downloaded) {
|
||||||
|
prefetchCodeForInstallation(tab.id, url);
|
||||||
|
}
|
||||||
|
return wrapReject(openURL({
|
||||||
|
url: '/install-usercss.html' +
|
||||||
|
'?updateUrl=' + encodeURIComponent(url) +
|
||||||
|
'&tabId=' + tab.id +
|
||||||
|
(direct ? '&direct=yes' : ''),
|
||||||
|
index: tab.index + 1,
|
||||||
|
openerTabId: tab.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchCodeForInstallation(tabId, url) {
|
||||||
|
const key = 'tempUsercssCode' + tabId;
|
||||||
|
Promise.all([
|
||||||
|
download(url),
|
||||||
|
chromeLocal.setValue(key, {loading: true}),
|
||||||
|
]).then(([code]) => {
|
||||||
|
chromeLocal.setValue(key, code);
|
||||||
|
setTimeout(() => chromeLocal.remove(key), 60e3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {build, save, findDup, openInstallPage};
|
||||||
|
})();
|
|
@ -1,141 +0,0 @@
|
||||||
/* global RX_META URLS download openURL */// toolbox.js
|
|
||||||
/* global addAPI bgReady */// common.js
|
|
||||||
/* global tabMan */// msg.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
bgReady.all.then(() => {
|
|
||||||
const installCodeCache = {};
|
|
||||||
|
|
||||||
addAPI(/** @namespace API */ {
|
|
||||||
usercss: {
|
|
||||||
getInstallCode(url) {
|
|
||||||
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
|
|
||||||
const {code, timer} = installCodeCache[url];
|
|
||||||
clearInstallCode(url);
|
|
||||||
clearTimeout(timer);
|
|
||||||
return code;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// `glob`: pathname match pattern for webRequest
|
|
||||||
// `rx`: pathname regex to verify the URL really looks like a raw usercss
|
|
||||||
const maybeDistro = {
|
|
||||||
// https://github.com/StylishThemes/GitHub-Dark/raw/master/github-dark.user.css
|
|
||||||
'github.com': {
|
|
||||||
glob: '/*/raw/*',
|
|
||||||
rx: /^\/[^/]+\/[^/]+\/raw\/[^/]+\/[^/]+?\.user\.(css|styl)$/,
|
|
||||||
},
|
|
||||||
// https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/github-dark.user.css
|
|
||||||
'raw.githubusercontent.com': {
|
|
||||||
glob: '/*',
|
|
||||||
rx: /^(\/[^/]+?){4}\.user\.(css|styl)$/,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
|
|
||||||
urls: [
|
|
||||||
URLS.usw + 'api/style/*.user.css',
|
|
||||||
...URLS.usoArchiveRaw.map(s => s + 'usercss/*.user.css'),
|
|
||||||
...['greasy', 'sleazy'].map(s => `*://${s}fork.org/scripts/*/code/*.user.css`),
|
|
||||||
...[].concat(
|
|
||||||
...Object.entries(maybeDistro)
|
|
||||||
.map(([host, {glob}]) => makeUsercssGlobs(host, glob))),
|
|
||||||
],
|
|
||||||
types: ['main_frame'],
|
|
||||||
}, ['blocking']);
|
|
||||||
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(rememberContentType, {
|
|
||||||
urls: makeUsercssGlobs('*', '/*'),
|
|
||||||
types: ['main_frame'],
|
|
||||||
}, ['responseHeaders']);
|
|
||||||
|
|
||||||
tabMan.onUpdate(maybeInstall);
|
|
||||||
|
|
||||||
function clearInstallCode(url) {
|
|
||||||
return delete installCodeCache[url];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sites may be using custom types like text/stylus so this coarse filter only excludes html */
|
|
||||||
function isContentTypeText(type) {
|
|
||||||
return /^text\/(?!html)/i.test(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// in Firefox we have to use a content script to read file://
|
|
||||||
async function loadFromFile(tabId) {
|
|
||||||
return (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFromUrl(tabId, url) {
|
|
||||||
return (
|
|
||||||
url.startsWith('file:') ||
|
|
||||||
tabMan.get(tabId, isContentTypeText.name) ||
|
|
||||||
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
|
|
||||||
) && download(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeInstallerUrl(url) {
|
|
||||||
return `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeUsercssGlobs(host, path) {
|
|
||||||
return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function maybeInstall({tabId, url, oldUrl = ''}) {
|
|
||||||
if (url.includes('.user.') &&
|
|
||||||
/^(https?|file|ftps?):/.test(url) &&
|
|
||||||
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
|
|
||||||
!oldUrl.startsWith(makeInstallerUrl(url))) {
|
|
||||||
const inTab = url.startsWith('file:') && !chrome.app;
|
|
||||||
const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
|
|
||||||
if (!/^\s*</.test(code) && RX_META.test(code)) {
|
|
||||||
await openInstallerPage(tabId, url, {code, inTab});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Faster installation on known distribution sites to avoid flicker of css text */
|
|
||||||
function maybeInstallFromDistro({tabId, url}) {
|
|
||||||
const u = new URL(url);
|
|
||||||
const m = maybeDistro[u.hostname];
|
|
||||||
if (!m || m.rx.test(u.pathname)) {
|
|
||||||
openInstallerPage(tabId, url, {});
|
|
||||||
// Silently suppress navigation.
|
|
||||||
// Don't redirect to the install URL as it'll flash the text!
|
|
||||||
return {cancel: true};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openInstallerPage(tabId, url, {code, inTab} = {}) {
|
|
||||||
const newUrl = makeInstallerUrl(url);
|
|
||||||
if (inTab) {
|
|
||||||
const tab = await browser.tabs.get(tabId);
|
|
||||||
return openURL({
|
|
||||||
url: `${newUrl}&tabId=${tabId}`,
|
|
||||||
active: tab.active,
|
|
||||||
index: tab.index + 1,
|
|
||||||
openerTabId: tabId,
|
|
||||||
currentWindow: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const timer = setTimeout(clearInstallCode, 10e3, url);
|
|
||||||
installCodeCache[url] = {code, timer};
|
|
||||||
try {
|
|
||||||
await browser.tabs.update(tabId, {url: newUrl});
|
|
||||||
} catch (err) {
|
|
||||||
// FIXME: remove this when kiwi supports tabs.update
|
|
||||||
// https://github.com/openstyles/stylus/issues/1367
|
|
||||||
if (/Tabs cannot be edited right now/i.test(err.message)) {
|
|
||||||
return browser.tabs.create({url: newUrl});
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remember Content-Type to avoid wasting time to re-fetch in loadFromUrl **/
|
|
||||||
function rememberContentType({tabId, responseHeaders}) {
|
|
||||||
const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
|
|
||||||
tabMan.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,163 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
/* global RX_META deepCopy download */// toolbox.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const usercssMan = {
|
|
||||||
|
|
||||||
GLOBAL_META: Object.entries({
|
|
||||||
author: null,
|
|
||||||
description: null,
|
|
||||||
homepageURL: 'url',
|
|
||||||
updateURL: 'updateUrl',
|
|
||||||
name: null,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/** `src` is a style or vars */
|
|
||||||
async assignVars(style, src) {
|
|
||||||
const meta = style.usercssData;
|
|
||||||
const meta2 = src.usercssData;
|
|
||||||
const {vars} = meta;
|
|
||||||
const oldVars = meta2 ? meta2.vars : src;
|
|
||||||
if (vars && oldVars) {
|
|
||||||
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
|
||||||
for (const [key, v] of Object.entries(vars)) {
|
|
||||||
const old = oldVars[key] && oldVars[key].value;
|
|
||||||
if (old) v.value = old;
|
|
||||||
}
|
|
||||||
meta.vars = await API.worker.nullifyInvalidVars(vars);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async build({
|
|
||||||
styleId,
|
|
||||||
sourceCode,
|
|
||||||
vars,
|
|
||||||
checkDup,
|
|
||||||
metaOnly,
|
|
||||||
assignVars,
|
|
||||||
initialUrl,
|
|
||||||
}) {
|
|
||||||
// downloading here while install-usercss page is loading to avoid the wait
|
|
||||||
if (initialUrl) sourceCode = await download(initialUrl);
|
|
||||||
const style = await usercssMan.buildMeta({sourceCode});
|
|
||||||
const dup = (checkDup || assignVars) &&
|
|
||||||
await usercssMan.find(styleId ? {id: styleId} : style);
|
|
||||||
let log;
|
|
||||||
if (!metaOnly) {
|
|
||||||
if (vars || assignVars) {
|
|
||||||
await usercssMan.assignVars(style, vars || dup);
|
|
||||||
}
|
|
||||||
await usercssMan.buildCode(style);
|
|
||||||
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
|
|
||||||
}
|
|
||||||
return {style, dup, log};
|
|
||||||
},
|
|
||||||
|
|
||||||
async buildCode(style) {
|
|
||||||
const {sourceCode: code, usercssData: {vars, preprocessor}} = style;
|
|
||||||
const match = code.match(RX_META);
|
|
||||||
const i = match.index;
|
|
||||||
const j = i + match[0].length;
|
|
||||||
const codeNoMeta = code.slice(0, i) + blankOut(code, i, j) + code.slice(j);
|
|
||||||
const {sections, errors, log} = await API.worker.compileUsercss(preprocessor, codeNoMeta, vars);
|
|
||||||
const recoverable = errors.every(e => e.recoverable);
|
|
||||||
if (!sections.length || !recoverable) {
|
|
||||||
throw !recoverable ? errors : 'Style does not contain any actual CSS to apply.';
|
|
||||||
}
|
|
||||||
style.sections = sections;
|
|
||||||
// adding a non-enumerable prop so it won't be written to storage
|
|
||||||
if (log) Object.defineProperty(style, 'log', {value: log});
|
|
||||||
return style;
|
|
||||||
},
|
|
||||||
|
|
||||||
async buildMeta(style) {
|
|
||||||
if (style.usercssData) {
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
// remember normalized sourceCode
|
|
||||||
let code = style.sourceCode = style.sourceCode.replace(/\r\n?/g, '\n');
|
|
||||||
style = Object.assign({
|
|
||||||
enabled: true,
|
|
||||||
sections: [],
|
|
||||||
}, style);
|
|
||||||
const match = code.match(RX_META);
|
|
||||||
if (!match) {
|
|
||||||
return Promise.reject(new Error('Could not find metadata.'));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
code = blankOut(code, 0, match.index) + match[0];
|
|
||||||
const {metadata} = await API.worker.parseUsercssMeta(code);
|
|
||||||
style.usercssData = metadata;
|
|
||||||
// https://github.com/openstyles/stylus/issues/560#issuecomment-440561196
|
|
||||||
for (const [key, globalKey] of usercssMan.GLOBAL_META) {
|
|
||||||
const val = metadata[key];
|
|
||||||
if (val !== undefined) {
|
|
||||||
style[globalKey || key] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code) {
|
|
||||||
const args = err.code === 'missingMandatory' || err.code === 'missingChar'
|
|
||||||
? err.args.map(e => e.length === 1 ? JSON.stringify(e) : e).join(', ')
|
|
||||||
: err.args;
|
|
||||||
const msg = chrome.i18n.getMessage(`meta_${(err.code)}`, args);
|
|
||||||
if (msg) err.message = msg;
|
|
||||||
}
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async configVars(id, vars) {
|
|
||||||
const style = deepCopy(await API.styles.get(id));
|
|
||||||
style.usercssData.vars = vars;
|
|
||||||
await usercssMan.buildCode(style);
|
|
||||||
return (await API.styles.install(style, 'config'))
|
|
||||||
.usercssData.vars;
|
|
||||||
},
|
|
||||||
|
|
||||||
async editSave(style) {
|
|
||||||
style = await usercssMan.parse(style);
|
|
||||||
return {
|
|
||||||
log: style.log, // extracting the non-enumerable prop, otherwise it won't survive messaging
|
|
||||||
style: await API.styles.editSave(style),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async find(styleOrData) {
|
|
||||||
if (styleOrData.id) {
|
|
||||||
return API.styles.get(styleOrData.id);
|
|
||||||
}
|
|
||||||
const {name, namespace} = styleOrData.usercssData || styleOrData;
|
|
||||||
for (const dup of await API.styles.getAll()) {
|
|
||||||
const data = dup.usercssData;
|
|
||||||
if (data &&
|
|
||||||
data.name === name &&
|
|
||||||
data.namespace === namespace) {
|
|
||||||
return dup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async install(style, opts) {
|
|
||||||
return API.styles.install(await usercssMan.parse(style, opts));
|
|
||||||
},
|
|
||||||
|
|
||||||
async parse(style, {dup, vars} = {}) {
|
|
||||||
style = await usercssMan.buildMeta(style);
|
|
||||||
// preserve style.vars during update
|
|
||||||
if (dup || (dup = await usercssMan.find(style))) {
|
|
||||||
style.id = dup.id;
|
|
||||||
}
|
|
||||||
if (vars || (vars = dup)) {
|
|
||||||
await usercssMan.assignVars(style, vars);
|
|
||||||
}
|
|
||||||
return usercssMan.buildCode(style);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Replaces everything with spaces to keep the original length,
|
|
||||||
* but preserves the line breaks to keep the original line/col relation */
|
|
||||||
function blankOut(str, start = 0, end = str.length) {
|
|
||||||
return str.slice(start, end).replace(/[^\r\n]/g, ' ');
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/* global URLS stringAsRegExp */// toolbox.js
|
|
||||||
/* global usercssMan */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const usoApi = {};
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const pingers = {};
|
|
||||||
|
|
||||||
usoApi.pingback = (usoId, delay) => {
|
|
||||||
clearTimeout(pingers[usoId]);
|
|
||||||
delete pingers[usoId];
|
|
||||||
if (delay > 0) {
|
|
||||||
return new Promise(resolve => (pingers[usoId] = setTimeout(ping, delay, usoId, resolve)));
|
|
||||||
} else if (delay !== false) {
|
|
||||||
return ping(usoId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replicating USO-Archive format
|
|
||||||
* https://github.com/33kk/uso-archive/blob/flomaster/lib/uso.js
|
|
||||||
* https://github.com/33kk/uso-archive/blob/flomaster/lib/converters.js
|
|
||||||
*/
|
|
||||||
usoApi.toUsercss = async (data, {metaOnly = true, varsUrl} = {}) => {
|
|
||||||
const badKeys = {};
|
|
||||||
const newKeys = [];
|
|
||||||
const descr = JSON.stringify(data.description.trim());
|
|
||||||
const vars = (data.style_settings || []).map(makeVar, {badKeys, newKeys}).join('');
|
|
||||||
const sourceCode = `\
|
|
||||||
/* ==UserStyle==
|
|
||||||
@name ${data.name}
|
|
||||||
@namespace USO Archive
|
|
||||||
@version ${data.updated.replace(/-/g, '').replace(/[T:]/g, '.').slice(0, 14)}
|
|
||||||
@description ${/^"['`]|\\/.test(descr) ? descr : descr.slice(1, -1)}
|
|
||||||
@author ${(data.user || {}).name || '?'}
|
|
||||||
@license ${makeLicense(data.license)}${vars ? '\n@preprocessor uso' + vars : ''}`
|
|
||||||
.replace(/\*\//g, '*\\/') +
|
|
||||||
`==/UserStyle== */\n${newKeys[0] ? useNewKeys(data.css, badKeys) : data.css}`;
|
|
||||||
const {style} = await usercssMan.build({sourceCode, metaOnly});
|
|
||||||
usoApi.useVarsUrl(style, varsUrl);
|
|
||||||
return {style, badKeys, newKeys};
|
|
||||||
};
|
|
||||||
|
|
||||||
usoApi.useVarsUrl = (style, url) => {
|
|
||||||
if (!/\?ik-/.test(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cfg = {badKeys: {}, newKeys: []};
|
|
||||||
const {vars} = style.usercssData;
|
|
||||||
if (!vars) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let [key, val] of new URLSearchParams(url.split('?')[1])) {
|
|
||||||
if (!key.startsWith('ik-')) continue;
|
|
||||||
key = makeKey(key.slice(3), cfg);
|
|
||||||
const v = vars[key];
|
|
||||||
if (!v) continue;
|
|
||||||
if (v.options) {
|
|
||||||
let sel = val.startsWith('ik-') && optByName(v, makeKey(val.slice(3), cfg));
|
|
||||||
if (!sel) {
|
|
||||||
key += '-custom';
|
|
||||||
sel = optByName(v, key + '-dropdown');
|
|
||||||
if (sel) vars[key].value = val;
|
|
||||||
}
|
|
||||||
if (sel) v.value = sel.name;
|
|
||||||
} else {
|
|
||||||
v.value = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function ping(id, resolve) {
|
|
||||||
await fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`);
|
|
||||||
if (resolve) resolve(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeKey(key, {badKeys, newKeys}) {
|
|
||||||
let res = badKeys[key];
|
|
||||||
if (!res) {
|
|
||||||
res = key.replace(/[^-\w]/g, '-');
|
|
||||||
res += newKeys.includes(res) ? '-' : '';
|
|
||||||
if (key !== res) {
|
|
||||||
badKeys[key] = res;
|
|
||||||
newKeys.push(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeLicense(s) {
|
|
||||||
return !s ? 'NO-REDISTRIBUTION' :
|
|
||||||
s === 'publicdomain' ? 'CC0-1.0' :
|
|
||||||
s.startsWith('ccby') ? `${s.toUpperCase().match(/(..)/g).join('-')}-4.0` :
|
|
||||||
s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeVar({
|
|
||||||
label,
|
|
||||||
setting_type: type,
|
|
||||||
install_key: ik,
|
|
||||||
style_setting_options: opts,
|
|
||||||
}) {
|
|
||||||
const cfg = this;
|
|
||||||
let value, suffix;
|
|
||||||
ik = makeKey(ik, cfg);
|
|
||||||
label = JSON.stringify(label);
|
|
||||||
switch (type) {
|
|
||||||
|
|
||||||
case 'color':
|
|
||||||
value = opts[0].value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
value = JSON.stringify(opts[0].value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'image': {
|
|
||||||
const ikCust = `${ik}-custom`;
|
|
||||||
opts.push({
|
|
||||||
label: 'Custom',
|
|
||||||
install_key: `${ikCust}-dropdown`,
|
|
||||||
value: `/*[[${ikCust}]]*/`,
|
|
||||||
});
|
|
||||||
suffix = `\n@advanced text ${ikCust} ${label.slice(0, -1)} (Custom)" "https://foo.com/123.jpg"`;
|
|
||||||
type = 'dropdown';
|
|
||||||
} // fallthrough
|
|
||||||
|
|
||||||
case 'dropdown':
|
|
||||||
value = '';
|
|
||||||
for (const o of opts) {
|
|
||||||
const def = o.default ? '*' : '';
|
|
||||||
const val = o.value;
|
|
||||||
const s = ` ${makeKey(o.install_key, cfg)} ${JSON.stringify(o.label + def)} <<<EOT${
|
|
||||||
val.includes('\n') ? '\n' : ' '}${val} EOT;\n`;
|
|
||||||
value = def ? s + value : value + s;
|
|
||||||
}
|
|
||||||
value = `{\n${value}}`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
value = '"ERROR: unknown type"';
|
|
||||||
}
|
|
||||||
return `\n@advanced ${type} ${ik} ${label} ${value}${suffix || ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function optByName(v, name) {
|
|
||||||
return v.options.find(o => o.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useNewKeys(css, badKeys) {
|
|
||||||
const rxsKeys = stringAsRegExp(Object.keys(badKeys).join('\n'), '', true).replace(/\n/g, '|');
|
|
||||||
const rxUsoVars = new RegExp(`(/\\*\\[\\[)(${rxsKeys})(?=]]\\*/)`, 'g');
|
|
||||||
return css.replace(rxUsoVars, (s, a, key) => a + badKeys[key]);
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,120 +0,0 @@
|
||||||
/* global API msg */// msg.js
|
|
||||||
/* global URLS */ // toolbox.js
|
|
||||||
/* global tokenMan */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const uswApi = (() => {
|
|
||||||
|
|
||||||
//#region Internals
|
|
||||||
|
|
||||||
class TokenHooks {
|
|
||||||
constructor(id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
keyName(name) {
|
|
||||||
return `${name}/${this.id}`;
|
|
||||||
}
|
|
||||||
query(query) {
|
|
||||||
return Object.assign(query, {vendor_data: this.id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fakeUsercssHeader(style) {
|
|
||||||
const {name, _usw: u = {}} = style;
|
|
||||||
const meta = Object.entries({
|
|
||||||
'@name': u.name || name || '?',
|
|
||||||
'@version': // Same as USO-archive version: YYYYMMDD.hh.mm
|
|
||||||
new Date().toISOString().replace(/^(\d+)-(\d+)-(\d+)T(\d+):(\d+).+/, '$1$2$3.$4.$5'),
|
|
||||||
'@namespace': u.namespace !== '?' && u.namespace ||
|
|
||||||
u.username && `userstyles.world/user/${u.username}` ||
|
|
||||||
'?',
|
|
||||||
'@description': u.description,
|
|
||||||
'@author': u.username,
|
|
||||||
'@license': u.license,
|
|
||||||
});
|
|
||||||
const maxKeyLen = meta.reduce((res, [k]) => Math.max(res, k.length), 0);
|
|
||||||
return [
|
|
||||||
'/* ==UserStyle==',
|
|
||||||
...meta.map(([k, v]) => v && `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v}`).filter(Boolean),
|
|
||||||
'==/UserStyle== */',
|
|
||||||
].join('\n') + '\n\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function linkStyle(style, sourceCode) {
|
|
||||||
const {id} = style;
|
|
||||||
const metadata = await API.worker.parseUsercssMeta(sourceCode).catch(console.warn) || {};
|
|
||||||
const uswData = Object.assign({}, style, {metadata, sourceCode});
|
|
||||||
API.data.set('usw' + id, uswData);
|
|
||||||
const token = await tokenMan.getToken('userstylesworld', true, new TokenHooks(id));
|
|
||||||
const info = await uswFetch('style', token);
|
|
||||||
const data = style._usw = Object.assign({token}, info);
|
|
||||||
style.url = style.url || data.homepage || `${URLS.usw}style/${data.id}`;
|
|
||||||
await uswSave(style);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uswFetch(path, token, opts) {
|
|
||||||
opts = Object.assign({credentials: 'omit'}, opts);
|
|
||||||
opts.headers = Object.assign({Authorization: `Bearer ${token}`}, opts.headers);
|
|
||||||
return (await (await fetch(`${URLS.usw}api/${path}`, opts)).json()).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Uses a custom method when broadcasting and avoids needlessly sending the entire style */
|
|
||||||
async function uswSave(style) {
|
|
||||||
const {id, _usw} = style;
|
|
||||||
await API.styles.save(style, {broadcast: false});
|
|
||||||
msg.broadcastExtension({method: 'uswData', style: {id, _usw}});
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Exports
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* @param {number} id
|
|
||||||
* @param {string} sourceCode
|
|
||||||
* @return {Promise<string>}
|
|
||||||
*/
|
|
||||||
async publish(id, sourceCode) {
|
|
||||||
const style = await API.styles.get(id);
|
|
||||||
const code = style.usercssData ? sourceCode
|
|
||||||
: fakeUsercssHeader(style) + sourceCode;
|
|
||||||
const data = (style._usw || {}).token
|
|
||||||
? style._usw
|
|
||||||
: await linkStyle(style, code);
|
|
||||||
return uswFetch(`style/${data.id}`, data.token, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({code}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} id
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
async revoke(id) {
|
|
||||||
await tokenMan.revokeToken('userstylesworld', new TokenHooks(id));
|
|
||||||
const style = await API.styles.get(id);
|
|
||||||
if (style) {
|
|
||||||
style._usw = {};
|
|
||||||
await uswSave(style);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* Doing this outside so we don't break IDE's recognition of the exported methods in IIFE */
|
|
||||||
for (const [k, fn] of Object.entries(uswApi)) {
|
|
||||||
uswApi[k] = async (id, ...args) => {
|
|
||||||
API.data.set('usw' + id, true);
|
|
||||||
try {
|
|
||||||
/* Awaiting inside `try` so that `finally` runs when done */
|
|
||||||
return await fn(id, ...args);
|
|
||||||
} finally {
|
|
||||||
API.data.del('usw' + id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
644
content/apply.js
644
content/apply.js
|
@ -1,271 +1,485 @@
|
||||||
/* global API msg */// msg.js
|
/* eslint no-var: 0 */
|
||||||
/* global StyleInjector */
|
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
if (window.INJECTED === 1) return;
|
var ID_PREFIX = 'stylus-';
|
||||||
window.INJECTED = 1;
|
var ROOT = document.documentElement;
|
||||||
|
var isOwnPage = location.protocol.endsWith('-extension:');
|
||||||
|
var disableAll = false;
|
||||||
|
var exposeIframes = false;
|
||||||
|
var styleElements = new Map();
|
||||||
|
var disabledElements = new Map();
|
||||||
|
var retiredStyleTimers = new Map();
|
||||||
|
var docRewriteObserver;
|
||||||
|
var docRootObserver;
|
||||||
|
|
||||||
/** true -> when the page styles are received,
|
requestStyles();
|
||||||
* false -> when disableAll mode is on at start, the styles won't be sent
|
chrome.runtime.onMessage.addListener(applyOnMessage);
|
||||||
* so while disableAll lasts we can ignore messages about style updates because
|
window.applyOnMessage = applyOnMessage;
|
||||||
* the tab will explicitly ask for all styles in bulk when disableAll mode ends */
|
|
||||||
let hasStyles = false;
|
|
||||||
let isDisabled = false;
|
|
||||||
let isTab = !chrome.tabs || location.pathname !== '/popup.html';
|
|
||||||
const order = {main: [], prio: []};
|
|
||||||
const calcOrder = ({id}) =>
|
|
||||||
(order.prio[id] || 0) * 1e6 ||
|
|
||||||
order.main[id] ||
|
|
||||||
id + .5e6; // no order = at the end of `main`
|
|
||||||
const isFrame = window !== parent;
|
|
||||||
const isFrameAboutBlank = isFrame && location.href === 'about:blank';
|
|
||||||
const isUnstylable = !chrome.app && document instanceof XMLDocument;
|
|
||||||
const styleInjector = StyleInjector({
|
|
||||||
compare: (a, b) => calcOrder(a) - calcOrder(b),
|
|
||||||
onUpdate: onInjectorUpdate,
|
|
||||||
});
|
|
||||||
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
|
|
||||||
let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
|
|
||||||
location.href;
|
|
||||||
|
|
||||||
// save it now because chrome.runtime will be unavailable in the orphaned script
|
if (!isOwnPage) {
|
||||||
const orphanEventId = chrome.runtime.id;
|
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
||||||
let isOrphaned;
|
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||||
// firefox doesn't orphanize content scripts so the old elements stay
|
|
||||||
if (!chrome.app) styleInjector.clearOrphans();
|
|
||||||
|
|
||||||
/** @type chrome.runtime.Port */
|
|
||||||
let port;
|
|
||||||
let lazyBadge = isFrame;
|
|
||||||
let parentDomain;
|
|
||||||
|
|
||||||
/* about:blank iframes are often used by sites for file upload or background tasks
|
|
||||||
* and they may break if unexpected DOM stuff is present at `load` event
|
|
||||||
* so we'll add the styles only if the iframe becomes visible */
|
|
||||||
const xoEventId = `${Math.random()}`;
|
|
||||||
/** @type IntersectionObserver */
|
|
||||||
let xo;
|
|
||||||
window[Symbol.for('xo')] = (el, cb) => {
|
|
||||||
if (!xo) xo = new IntersectionObserver(onIntersect, {rootMargin: '100%'});
|
|
||||||
el.addEventListener(xoEventId, cb, {once: true});
|
|
||||||
xo.observe(el);
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME: move this to background page when following bugs are fixed:
|
|
||||||
// https://bugzil.la/1587723, https://crbug.com/968651
|
|
||||||
const mqDark = matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
mqDark.onchange = e => API.colorScheme.updateSystemPreferDark(e.matches);
|
|
||||||
mqDark.onchange(mqDark);
|
|
||||||
|
|
||||||
// Declare all vars before init() or it'll throw due to "temporal dead zone" of const/let
|
|
||||||
init();
|
|
||||||
|
|
||||||
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
|
|
||||||
if (!isTab) {
|
|
||||||
chrome.tabs.getCurrent(tab => {
|
|
||||||
isTab = Boolean(tab);
|
|
||||||
if (tab && styleInjector.list.length) updateCount();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.onTab(applyOnMessage);
|
function requestStyles(options, callback = applyStyles) {
|
||||||
window.addEventListener('pageshow', e => {
|
var matchUrl = location.href;
|
||||||
if (e.isTrusted && e.persisted) { // bfcache
|
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||||
updateCount();
|
// dynamic about: and javascript: iframes don't have an URL yet
|
||||||
|
// so we'll try the parent frame which is guaranteed to have a real URL
|
||||||
|
try {
|
||||||
|
if (window !== parent) {
|
||||||
|
matchUrl = parent.location.href;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
});
|
const request = Object.assign({
|
||||||
|
method: 'getStyles',
|
||||||
if (!chrome.tabs) {
|
matchUrl,
|
||||||
window.dispatchEvent(new CustomEvent(orphanEventId));
|
enabled: true,
|
||||||
window.addEventListener(orphanEventId, orphanCheck, true);
|
asHash: true,
|
||||||
}
|
}, options);
|
||||||
|
// On own pages we request the styles directly to minimize delay and flicker
|
||||||
function onInjectorUpdate() {
|
if (typeof getStylesSafe === 'function') {
|
||||||
if (!isOrphaned) {
|
getStylesSafe(request).then(callback);
|
||||||
updateCount();
|
|
||||||
const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
|
|
||||||
onOff('disableAll', updateDisableAll);
|
|
||||||
if (isFrame) {
|
|
||||||
updateExposeIframes();
|
|
||||||
onOff('exposeIframes', updateExposeIframes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
if (isUnstylable) {
|
|
||||||
await API.styleViaAPI({method: 'styleApply'});
|
|
||||||
} else {
|
} else {
|
||||||
const SYM_ID = 'styles';
|
chrome.runtime.sendMessage(request, callback);
|
||||||
const SYM = Symbol.for(SYM_ID);
|
|
||||||
const parentStyles = isFrameAboutBlank &&
|
|
||||||
tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
|
|
||||||
const styles =
|
|
||||||
window[SYM] ||
|
|
||||||
parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
|
|
||||||
!isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
|
|
||||||
await API.styles.getSectionsByUrl(matchUrl, null, true);
|
|
||||||
if (styles.cfg) {
|
|
||||||
isDisabled = styles.cfg.disableAll;
|
|
||||||
Object.assign(order, styles.cfg.order);
|
|
||||||
}
|
|
||||||
hasStyles = !isDisabled;
|
|
||||||
if (hasStyles) {
|
|
||||||
window[SYM] = styles;
|
|
||||||
await styleInjector.apply(styles);
|
|
||||||
} else {
|
|
||||||
delete window[SYM];
|
|
||||||
prefs.subscribe('disableAll', updateDisableAll);
|
|
||||||
}
|
|
||||||
styleInjector.toggle(hasStyles);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Must be executed inside try/catch */
|
|
||||||
function getStylesViaXhr() {
|
|
||||||
const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
|
|
||||||
const url = 'blob:' + chrome.runtime.getURL(blobId);
|
|
||||||
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('GET', url, false); // synchronous
|
|
||||||
xhr.send();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
return JSON.parse(xhr.response);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyOnMessage(request) {
|
function applyOnMessage(request, sender, sendResponse) {
|
||||||
const {method} = request;
|
if (request.styles === 'DIY') {
|
||||||
if (isUnstylable) {
|
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
||||||
if (method === 'urlChanged') {
|
// which is faster because IPC messaging JSON-ifies everything internally
|
||||||
request.method = 'styleReplaceAll';
|
requestStyles({}, styles => {
|
||||||
}
|
request.styles = styles;
|
||||||
if (/^(style|updateCount)/.test(method)) {
|
applyOnMessage(request);
|
||||||
API.styleViaAPI(request);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {style} = request;
|
switch (request.method) {
|
||||||
switch (method) {
|
|
||||||
case 'ping':
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
styleInjector.remove(style.id);
|
removeStyle(request);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (!hasStyles && isDisabled) break;
|
if (request.codeIsUpdated === false) {
|
||||||
if (style.enabled) {
|
applyStyleState(request.style);
|
||||||
API.styles.getSectionsByUrl(matchUrl, style.id).then(sections =>
|
break;
|
||||||
sections[style.id]
|
}
|
||||||
? styleInjector.apply(sections)
|
if (request.style.enabled) {
|
||||||
: styleInjector.remove(style.id));
|
removeStyle({id: request.style.id, retire: true});
|
||||||
|
requestStyles({id: request.style.id});
|
||||||
} else {
|
} else {
|
||||||
styleInjector.remove(style.id);
|
removeStyle(request.style);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
if (!hasStyles && isDisabled) break;
|
if (request.style.enabled) {
|
||||||
if (style.enabled) {
|
requestStyles({id: request.style.id});
|
||||||
API.styles.getSectionsByUrl(matchUrl, style.id)
|
|
||||||
.then(styleInjector.apply);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleSort':
|
case 'styleApply':
|
||||||
Object.assign(order, request.order);
|
applyStyles(request.styles);
|
||||||
styleInjector.sort();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'urlChanged':
|
case 'styleReplaceAll':
|
||||||
if (!hasStyles && isDisabled || matchUrl === request.url) break;
|
replaceAll(request.styles);
|
||||||
matchUrl = request.url;
|
|
||||||
API.styles.getSectionsByUrl(matchUrl).then(sections => {
|
|
||||||
hasStyles = true;
|
|
||||||
styleInjector.replace(sections);
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'updateCount':
|
case 'prefChanged':
|
||||||
updateCount();
|
if ('disableAll' in request.prefs) {
|
||||||
|
doDisableAll(request.prefs.disableAll);
|
||||||
|
}
|
||||||
|
if ('exposeIframes' in request.prefs) {
|
||||||
|
doExposeIframes(request.prefs.exposeIframes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ping':
|
||||||
|
sendResponse(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDisableAll(key, disableAll) {
|
|
||||||
isDisabled = disableAll;
|
function doDisableAll(disable = disableAll) {
|
||||||
if (isUnstylable) {
|
if (!disable === !disableAll) {
|
||||||
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
return;
|
||||||
} else if (!hasStyles && !disableAll) {
|
}
|
||||||
init();
|
disableAll = disable;
|
||||||
|
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
||||||
|
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
|
||||||
|
&& stylesheet.disabled !== disable) {
|
||||||
|
stylesheet.disabled = disable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function doExposeIframes(state = exposeIframes) {
|
||||||
|
if (state === exposeIframes || window === parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exposeIframes = state;
|
||||||
|
const attr = document.documentElement.getAttribute('stylus-iframe');
|
||||||
|
if (state && attr !== '') {
|
||||||
|
document.documentElement.setAttribute('stylus-iframe', '');
|
||||||
|
} else if (!state && attr === '') {
|
||||||
|
document.documentElement.removeAttribute('stylus-iframe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyStyleState({id, enabled}) {
|
||||||
|
const inCache = disabledElements.get(id) || styleElements.get(id);
|
||||||
|
const inDoc = document.getElementById(ID_PREFIX + id);
|
||||||
|
if (enabled) {
|
||||||
|
if (inDoc) {
|
||||||
|
return;
|
||||||
|
} else if (inCache) {
|
||||||
|
addStyleElement(inCache);
|
||||||
|
disabledElements.delete(id);
|
||||||
|
} else {
|
||||||
|
requestStyles({id});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
styleInjector.toggle(!disableAll);
|
if (inDoc) {
|
||||||
}
|
disabledElements.set(id, inDoc);
|
||||||
}
|
docRootObserver.stop();
|
||||||
|
inDoc.remove();
|
||||||
async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
|
docRootObserver.start();
|
||||||
const attr = 'stylus-iframe';
|
|
||||||
const el = document.documentElement;
|
|
||||||
if (!el) return; // got no styles so styleInjector didn't wait for <html>
|
|
||||||
if (!value || !styleInjector.list.length) {
|
|
||||||
el.removeAttribute(attr);
|
|
||||||
} else {
|
|
||||||
if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
|
|
||||||
// Check first to avoid triggering DOM mutation
|
|
||||||
if (el.getAttribute(attr) !== parentDomain) {
|
|
||||||
el.setAttribute(attr, parentDomain);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCount() {
|
|
||||||
if (!isTab) return;
|
function removeStyle({id, retire = false}) {
|
||||||
if (isFrame) {
|
const el = document.getElementById(ID_PREFIX + id);
|
||||||
if (!port && styleInjector.list.length) {
|
if (el) {
|
||||||
port = chrome.runtime.connect({name: 'iframe'});
|
if (retire) {
|
||||||
} else if (port && !styleInjector.list.length) {
|
// to avoid page flicker when the style is updated
|
||||||
port.disconnect();
|
// instead of removing it immediately we rename its ID and queue it
|
||||||
|
// to be deleted in applyStyles after a new version is fetched and applied
|
||||||
|
const deadID = 'ghost-' + id;
|
||||||
|
el.id = ID_PREFIX + deadID;
|
||||||
|
// in case something went wrong and new style was never applied
|
||||||
|
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
|
||||||
|
} else {
|
||||||
|
el.remove();
|
||||||
}
|
}
|
||||||
if (lazyBadge && performance.now() > 1000) lazyBadge = false;
|
|
||||||
}
|
}
|
||||||
(isUnstylable ?
|
styleElements.delete(ID_PREFIX + id);
|
||||||
API.styleViaAPI({method: 'updateCount'}) :
|
disabledElements.delete(id);
|
||||||
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
|
retiredStyleTimers.delete(id);
|
||||||
).catch(msg.ignoreError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFrameElementInView(cb) {
|
|
||||||
parent[parent.Symbol.for('xo')](frameElement, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {IntersectionObserverEntry[]} entries */
|
function applyStyles(styles) {
|
||||||
function onIntersect(entries) {
|
if (!styles) {
|
||||||
for (const e of entries) {
|
// Chrome is starting up
|
||||||
if (e.isIntersecting) {
|
requestStyles();
|
||||||
xo.unobserve(e.target);
|
return;
|
||||||
e.target.dispatchEvent(new Event(xoEventId));
|
}
|
||||||
|
if ('disableAll' in styles) {
|
||||||
|
doDisableAll(styles.disableAll);
|
||||||
|
delete styles.disableAll;
|
||||||
|
}
|
||||||
|
if ('exposeIframes' in styles) {
|
||||||
|
doExposeIframes(styles.exposeIframes);
|
||||||
|
delete styles.exposeIframes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gotNewStyles = Object.keys(styles).length || styles.needTransitionPatch;
|
||||||
|
if (gotNewStyles) {
|
||||||
|
if (docRootObserver) {
|
||||||
|
docRootObserver.stop();
|
||||||
|
} else {
|
||||||
|
initDocRootObserver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (styles.needTransitionPatch) {
|
||||||
|
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||||
|
// the browsers, especially Firefox, may apply all transitions on page load
|
||||||
|
delete styles.needTransitionPatch;
|
||||||
|
const className = chrome.runtime.id + '-transition-bug-fix';
|
||||||
|
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
|
||||||
|
document.documentElement.classList.add(className);
|
||||||
|
applySections(0, `
|
||||||
|
${docId}.${className}:root * {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
setTimeout(() => {
|
||||||
|
removeStyle({id: 0});
|
||||||
|
document.documentElement.classList.remove(className);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gotNewStyles) {
|
||||||
|
for (const id in styles) {
|
||||||
|
applySections(id, styles[id].map(section => section.code).join('\n'));
|
||||||
|
}
|
||||||
|
docRootObserver.start({sort: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
|
||||||
|
initDocRewriteObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retiredStyleTimers.size) {
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const [id, timer] of retiredStyleTimers.entries()) {
|
||||||
|
removeStyle({id});
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryCatch(func, ...args) {
|
|
||||||
try {
|
function applySections(styleId, code) {
|
||||||
return func(...args);
|
const id = ID_PREFIX + styleId;
|
||||||
} catch (e) {}
|
let el = styleElements.get(id) || document.getElementById(id);
|
||||||
|
if (!el) {
|
||||||
|
if (document.documentElement instanceof SVGSVGElement) {
|
||||||
|
// SVG document style
|
||||||
|
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||||
|
} else if (document instanceof XMLDocument) {
|
||||||
|
// XML document style
|
||||||
|
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
|
||||||
|
} else {
|
||||||
|
// HTML document style; also works on HTML-embedded SVG
|
||||||
|
el = document.createElement('style');
|
||||||
|
}
|
||||||
|
Object.assign(el, {
|
||||||
|
id,
|
||||||
|
type: 'text/css',
|
||||||
|
textContent: code,
|
||||||
|
});
|
||||||
|
// SVG className is not a string, but an instance of SVGAnimatedString
|
||||||
|
el.classList.add('stylus');
|
||||||
|
addStyleElement(el);
|
||||||
|
}
|
||||||
|
styleElements.set(id, el);
|
||||||
|
disabledElements.delete(Number(styleId));
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addStyleElement(newElement) {
|
||||||
|
if (!ROOT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let next;
|
||||||
|
const newStyleId = getStyleId(newElement);
|
||||||
|
for (const el of styleElements.values()) {
|
||||||
|
if (el.parentNode && !el.id.endsWith('-ghost') && getStyleId(el) > newStyleId) {
|
||||||
|
next = el.parentNode === ROOT ? el : null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (next === newElement.nextElementSibling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
docRootObserver.stop();
|
||||||
|
ROOT.insertBefore(newElement, next || null);
|
||||||
|
if (disableAll) {
|
||||||
|
newElement.disabled = true;
|
||||||
|
}
|
||||||
|
docRootObserver.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function replaceAll(newStyles) {
|
||||||
|
const oldStyles = Array.prototype.slice.call(
|
||||||
|
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
|
||||||
|
oldStyles.forEach(el => (el.id += '-ghost'));
|
||||||
|
styleElements.clear();
|
||||||
|
disabledElements.clear();
|
||||||
|
[...retiredStyleTimers.values()].forEach(clearTimeout);
|
||||||
|
retiredStyleTimers.clear();
|
||||||
|
applyStyles(newStyles);
|
||||||
|
oldStyles.forEach(el => el.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getStyleId(el) {
|
||||||
|
return parseInt(el.id.substr(ID_PREFIX.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function orphanCheck() {
|
function orphanCheck() {
|
||||||
if (chrome.runtime.id) return;
|
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// In Chrome content script is orphaned on an extension update/reload
|
// In Chrome content script is orphaned on an extension update/reload
|
||||||
// so we need to detach event listeners
|
// so we need to detach event listeners
|
||||||
window.removeEventListener(orphanEventId, orphanCheck, true);
|
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.takeRecords() && ob.disconnect());
|
||||||
mqDark.onchange = null;
|
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||||
isOrphaned = true;
|
}
|
||||||
setTimeout(styleInjector.clear, 1000); // avoiding FOUC
|
|
||||||
tryCatch(msg.off, applyOnMessage);
|
|
||||||
|
function initDocRewriteObserver() {
|
||||||
|
// detect documentElement being rewritten from inside the script
|
||||||
|
docRewriteObserver = new MutationObserver(mutations => {
|
||||||
|
for (let m = mutations.length; --m >= 0;) {
|
||||||
|
const added = mutations[m].addedNodes;
|
||||||
|
for (let n = added.length; --n >= 0;) {
|
||||||
|
if (added[n].localName === 'html') {
|
||||||
|
reinjectStyles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
docRewriteObserver.observe(document, {childList: true});
|
||||||
|
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.documentElement !== ROOT) {
|
||||||
|
reinjectStyles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// re-add styles if we detect documentElement being recreated
|
||||||
|
function reinjectStyles() {
|
||||||
|
if (!styleElements) {
|
||||||
|
orphanCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ROOT = document.documentElement;
|
||||||
|
docRootObserver.stop();
|
||||||
|
const imported = [];
|
||||||
|
for (const [id, el] of styleElements.entries()) {
|
||||||
|
const copy = document.importNode(el, true);
|
||||||
|
el.textContent += ' '; // invalidate CSSOM cache
|
||||||
|
imported.push([id, copy]);
|
||||||
|
addStyleElement(copy);
|
||||||
|
}
|
||||||
|
docRootObserver.start();
|
||||||
|
styleElements = new Map(imported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initDocRootObserver() {
|
||||||
|
let lastRestorationTime = 0;
|
||||||
|
let restorationCounter = 0;
|
||||||
|
let observing = false;
|
||||||
|
let sorting = false;
|
||||||
|
// allow any types of elements between ours, except for the following:
|
||||||
|
const ORDERED_TAGS = ['head', 'body', 'frameset', 'style', 'link'];
|
||||||
|
|
||||||
|
init();
|
||||||
|
return;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
docRootObserver = new MutationObserver(sortStyleElements);
|
||||||
|
Object.assign(docRootObserver, {start, stop});
|
||||||
|
setTimeout(sortStyleElements);
|
||||||
|
}
|
||||||
|
function start({sort = false} = {}) {
|
||||||
|
if (sort && sortStyleMap()) {
|
||||||
|
sortStyleElements();
|
||||||
|
}
|
||||||
|
if (!observing && ROOT) {
|
||||||
|
docRootObserver.observe(ROOT, {childList: true});
|
||||||
|
observing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function stop() {
|
||||||
|
if (observing) {
|
||||||
|
docRootObserver.disconnect();
|
||||||
|
observing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function sortStyleMap() {
|
||||||
|
const list = [];
|
||||||
|
let prevStyleId = 0;
|
||||||
|
let needsSorting = false;
|
||||||
|
for (const entry of styleElements.entries()) {
|
||||||
|
list.push(entry);
|
||||||
|
const el = entry[1];
|
||||||
|
const styleId = getStyleId(el);
|
||||||
|
el.styleId = styleId;
|
||||||
|
needsSorting |= styleId < prevStyleId;
|
||||||
|
prevStyleId = styleId;
|
||||||
|
}
|
||||||
|
if (needsSorting) {
|
||||||
|
styleElements = new Map(list.sort((a, b) => a[1].styleId - b[1].styleId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function sortStyleElements() {
|
||||||
|
let prevExpected = document.documentElement.lastElementChild;
|
||||||
|
while (prevExpected && isSkippable(prevExpected, true)) {
|
||||||
|
prevExpected = prevExpected.previousElementSibling;
|
||||||
|
}
|
||||||
|
if (!prevExpected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const el of styleElements.values()) {
|
||||||
|
if (!isMovable(el)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
const next = prevExpected.nextElementSibling;
|
||||||
|
if (next && isSkippable(next)) {
|
||||||
|
prevExpected = next;
|
||||||
|
} else if (
|
||||||
|
next === el ||
|
||||||
|
next === el.previousElementSibling ||
|
||||||
|
moveAfter(el, next || prevExpected)) {
|
||||||
|
prevExpected = el;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sorting) {
|
||||||
|
sorting = false;
|
||||||
|
docRootObserver.takeRecords();
|
||||||
|
if (!restorationLimitExceeded()) {
|
||||||
|
start();
|
||||||
|
} else {
|
||||||
|
setTimeout(start, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isMovable(el) {
|
||||||
|
return el.parentNode || !disabledElements.has(getStyleId(el));
|
||||||
|
}
|
||||||
|
function isSkippable(el, skipOwnStyles) {
|
||||||
|
return !ORDERED_TAGS.includes(el.localName) ||
|
||||||
|
el.id.startsWith(ID_PREFIX) &&
|
||||||
|
(skipOwnStyles || el.id.endsWith('-ghost')) &&
|
||||||
|
el.localName === 'style' &&
|
||||||
|
el.className === 'stylus';
|
||||||
|
}
|
||||||
|
function moveAfter(el, expected) {
|
||||||
|
if (!sorting) {
|
||||||
|
sorting = true;
|
||||||
|
docRootObserver.stop();
|
||||||
|
}
|
||||||
|
expected.insertAdjacentElement('afterend', el);
|
||||||
|
if (el.disabled !== disableAll) {
|
||||||
|
// moving an element resets its 'disabled' state
|
||||||
|
el.disabled = disableAll;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function restorationLimitExceeded() {
|
||||||
|
const t = performance.now();
|
||||||
|
if (t - lastRestorationTime > 1000) {
|
||||||
|
restorationCounter = 0;
|
||||||
|
}
|
||||||
|
lastRestorationTime = t;
|
||||||
|
return ++restorationCounter > 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// onCommitted may fire twice
|
|
||||||
// Note, we're checking against a literal `1`, not just `if (truthy)`,
|
|
||||||
// because <html id="INJECTED"> is exposed per HTML spec as a global variable and `window.INJECTED`.
|
|
||||||
|
|
||||||
if (window.INJECTED_GREASYFORK !== 1) {
|
|
||||||
window.INJECTED_GREASYFORK = 1;
|
|
||||||
addEventListener('message', async function onMessage(e) {
|
|
||||||
if (e.origin === location.origin &&
|
|
||||||
e.data &&
|
|
||||||
e.data.name &&
|
|
||||||
e.data.type === 'style-version-query') {
|
|
||||||
removeEventListener('message', onMessage);
|
|
||||||
const style = await API.usercss.find(e.data) || {};
|
|
||||||
const {version} = style.usercssData || {};
|
|
||||||
postMessage({type: 'style-version', version}, '*');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,26 +1,117 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
|
function createSourceLoader() {
|
||||||
if (typeof window.oldCode !== 'string') {
|
let source;
|
||||||
window.oldCode = (document.querySelector('body > pre') || document.body).textContent;
|
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
function fetchText(url) {
|
||||||
if (port.name !== 'downloadSelf') return;
|
return new Promise((resolve, reject) => {
|
||||||
port.onMessage.addListener(async ({id, force}) => {
|
// you can't use fetch in Chrome under 'file:' protocol
|
||||||
const msg = {id};
|
const xhr = new XMLHttpRequest();
|
||||||
try {
|
xhr.open('GET', url);
|
||||||
const code = await (await fetch(location.href, {mode: 'same-origin'})).text();
|
xhr.addEventListener('load', () => resolve(xhr.responseText));
|
||||||
if (code !== window.oldCode || force) {
|
xhr.addEventListener('error', () => reject(xhr));
|
||||||
msg.code = window.oldCode = code;
|
xhr.send();
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
msg.error = error.message || `${error}`;
|
|
||||||
}
|
|
||||||
port.postMessage(msg);
|
|
||||||
});
|
});
|
||||||
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
|
}
|
||||||
addEventListener('pagehide', () => port.disconnect(), {once: true});
|
|
||||||
});
|
function load() {
|
||||||
|
return fetchText(location.href).then(newSource => {
|
||||||
|
source = newSource;
|
||||||
|
return source;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch(cb) {
|
||||||
|
let timer;
|
||||||
|
const DELAY = 1000;
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (timer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timer = setTimeout(check, DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
fetchText(location.href)
|
||||||
|
.then(newSource => {
|
||||||
|
if (source !== newSource) {
|
||||||
|
source = newSource;
|
||||||
|
return cb(source);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(t('liveReloadError', error));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
timer = setTimeout(check, DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {start, stop};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {load, watch, source: () => source};
|
||||||
}
|
}
|
||||||
|
|
||||||
// passing the result to tabs.executeScript
|
function initUsercssInstall() {
|
||||||
window.oldCode; // eslint-disable-line no-unused-expressions
|
const sourceLoader = createSourceLoader();
|
||||||
|
const pendingSource = sourceLoader.load();
|
||||||
|
let watcher;
|
||||||
|
|
||||||
|
chrome.runtime.onConnect.addListener(port => {
|
||||||
|
port.onMessage.addListener(msg => {
|
||||||
|
switch (msg.method) {
|
||||||
|
case 'getSourceCode':
|
||||||
|
pendingSource
|
||||||
|
.then(sourceCode => port.postMessage({method: msg.method + 'Response', sourceCode}))
|
||||||
|
.catch(err => port.postMessage({method: msg.method + 'Response', error: err.message || String(err)}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'liveReloadStart':
|
||||||
|
if (!watcher) {
|
||||||
|
watcher = sourceLoader.watch(sourceCode => {
|
||||||
|
port.postMessage({method: 'sourceCodeChanged', sourceCode});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
watcher.start();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'liveReloadStop':
|
||||||
|
watcher.stop();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'closeTab':
|
||||||
|
if (history.length > 1) {
|
||||||
|
history.back();
|
||||||
|
} else {
|
||||||
|
chrome.runtime.sendMessage({method: 'closeTab'});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
method: 'openUsercssInstallPage',
|
||||||
|
url: location.href,
|
||||||
|
}, r => r && r.__ERROR__ && alert(r.__ERROR__));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUsercss() {
|
||||||
|
if (!/text\/(css|plain)/.test(document.contentType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/==userstyle==/i.test(document.body.textContent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUsercss()) {
|
||||||
|
initUsercssInstall();
|
||||||
|
}
|
||||||
|
|
|
@ -1,324 +1,369 @@
|
||||||
/* global API */// msg.js
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
(() => {
|
||||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
|
const FIREFOX = !chrome.app;
|
||||||
if (window.INJECTED_USO === 1) return;
|
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
|
||||||
window.INJECTED_USO = 1;
|
const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
|
||||||
|
|
||||||
const usoId = RegExp.$1;
|
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
||||||
const USO = 'https://userstyles.org';
|
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||||
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
|
|
||||||
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
|
|
||||||
const CLICK = [
|
|
||||||
['#install_stylish_style_button', onInstall],
|
|
||||||
['#update_stylish_style_button', onInstall],
|
|
||||||
['.customize_style_button', onCustomize],
|
|
||||||
['.uninstall_stylish_style_button', onUninstall],
|
|
||||||
];
|
|
||||||
const pageEventId = `${performance.now()}${Math.random()}`;
|
|
||||||
const contentEventId = pageEventId + ':';
|
|
||||||
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
|
|
||||||
const $ = (sel, base = document) => base.querySelector(sel);
|
|
||||||
const toggleListener = (isOn, ...args) => (isOn ? addEventListener : removeEventListener)(...args);
|
|
||||||
const togglePageListener = isOn => toggleListener(isOn, contentEventId, onPageEvent, true);
|
|
||||||
|
|
||||||
const mo = new MutationObserver(onMutation);
|
['Update', 'Install'].forEach(type =>
|
||||||
const observeColors = isOn =>
|
['', 'Chrome', 'Opera'].forEach(browser =>
|
||||||
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
|
document.addEventListener('stylish' + type + browser, onClick)));
|
||||||
: mo.disconnect();
|
|
||||||
|
|
||||||
let style, dup, md5, pageData, badKeys;
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
// orphaned content script check
|
||||||
|
if (msg.method === 'ping') {
|
||||||
|
sendResponse(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
|
new MutationObserver((mutations, observer) => {
|
||||||
addEventListener(orphanEventId, orphanCheck, true);
|
if (document.body) {
|
||||||
addEventListener('click', onClick, true);
|
observer.disconnect();
|
||||||
togglePageListener(true);
|
// TODO: remove the following statement when USO pagination title is fixed
|
||||||
|
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
method: 'getStyles',
|
||||||
|
md5Url: getMeta('stylish-md5-url') || location.href
|
||||||
|
}, checkUpdatability);
|
||||||
|
}
|
||||||
|
}).observe(document.documentElement, {childList: true});
|
||||||
|
|
||||||
[md5, dup] = await Promise.all([
|
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||||
fetch(md5Url).then(r => r.text()),
|
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||||
API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
|
we need to fix this URL using "stylish-update-url" meta key
|
||||||
.then(sendVarsToPage),
|
*/
|
||||||
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
|
function getStyleURL() {
|
||||||
]);
|
const textUrl = getMeta('stylish-update-url') || '';
|
||||||
|
const jsonUrl = getMeta('stylish-code-chrome') ||
|
||||||
if (!dup) {
|
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
||||||
sendStylishEvent('styleCanBeInstalledChrome');
|
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
||||||
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
|
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||||
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
|
|
||||||
sendStylishEvent('styleCanBeUpdatedChrome');
|
|
||||||
} else {
|
|
||||||
sendStylishEvent('styleAlreadyInstalledChrome');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onClick(e) {
|
function checkUpdatability([installedStyle]) {
|
||||||
for (const [sel, fn] of CLICK) {
|
// TODO: remove the following statement when USO is fixed
|
||||||
const el = e.target.closest(sel);
|
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||||
if (!el) continue;
|
detail: installedStyle && installedStyle.updateUrl,
|
||||||
try {
|
}));
|
||||||
el.disabled = true;
|
if (!installedStyle) {
|
||||||
await fn(e);
|
sendEvent('styleCanBeInstalledChrome');
|
||||||
} catch (e) {
|
return;
|
||||||
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
|
}
|
||||||
} finally {
|
const md5Url = getMeta('stylish-md5-url');
|
||||||
el.disabled = false;
|
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||||
}
|
getResource(md5Url).then(md5 => {
|
||||||
|
reportUpdatable(md5 !== installedStyle.originalMd5);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
getStyleJson().then(json => {
|
||||||
|
reportUpdatable(!json ||
|
||||||
|
!styleSectionsEqual(json, installedStyle));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportUpdatable(isUpdatable) {
|
||||||
|
sendEvent(
|
||||||
|
isUpdatable
|
||||||
|
? 'styleCanBeUpdatedChrome'
|
||||||
|
: 'styleAlreadyInstalledChrome',
|
||||||
|
{
|
||||||
|
updateUrl: installedStyle.updateUrl
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCustomize() {
|
|
||||||
const ss = $('#style-settings');
|
|
||||||
const willShow = !ss || !ss.offsetHeight;
|
|
||||||
observeColors(willShow);
|
|
||||||
toggleListener(willShow, 'change', onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onInstall(e) {
|
function sendEvent(type, detail = null) {
|
||||||
const {id} = dup;
|
if (FIREFOX) {
|
||||||
e.stopPropagation();
|
type = type.replace('Chrome', '');
|
||||||
if (!style) await buildStyle();
|
} else if (OPERA || VIVALDI) {
|
||||||
style = dup = await API.usercss.install(style, {
|
type = type.replace('Chrome', 'Opera');
|
||||||
dup: {id},
|
|
||||||
vars: getPageVars(),
|
|
||||||
});
|
|
||||||
sendStylishEvent('styleInstalledChrome');
|
|
||||||
API.uso.pingback(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUninstall() {
|
|
||||||
const {id} = dup;
|
|
||||||
dup = style = false;
|
|
||||||
observeColors(false);
|
|
||||||
removeEventListener('change', onChange);
|
|
||||||
return API.styles.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange({target: el}) {
|
|
||||||
if (dup && el.matches('[name^="ik-"], [type=file]')) {
|
|
||||||
API.usercss.configVars(dup.id, getPageVars());
|
|
||||||
}
|
}
|
||||||
}
|
detail = {detail};
|
||||||
|
if (typeof cloneInto !== 'undefined') {
|
||||||
function onMutation(mutations) {
|
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||||
for (const {target: el} of mutations) {
|
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||||
if (el.style.display === 'none' &&
|
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
||||||
/^ik-/.test(el.name) &&
|
|
||||||
/^#[\da-f]{6}$/.test(el.value)) {
|
|
||||||
onChange({target: el});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
onDOMready().then(() => {
|
||||||
|
document.dispatchEvent(new CustomEvent(type, detail));
|
||||||
function onPageEvent(e) {
|
|
||||||
pageData = e.detail;
|
|
||||||
togglePageListener(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildStyle() {
|
|
||||||
if (!pageData) pageData = await (await fetch(apiUrl)).json();
|
|
||||||
({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
|
|
||||||
Object.assign(style, {
|
|
||||||
md5Url,
|
|
||||||
id: dup.id,
|
|
||||||
originalMd5: md5,
|
|
||||||
updateUrl: apiUrl,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPageVars() {
|
|
||||||
const {vars} = (style || dup).usercssData;
|
function onClick(event) {
|
||||||
for (const el of document.querySelectorAll('[name^="ik-"]')) {
|
if (onClick.processing || !orphanCheck()) {
|
||||||
const name = el.name.slice(3); // dropping "ik-"
|
return;
|
||||||
const ik = (badKeys || {})[name] || name;
|
}
|
||||||
const v = vars[ik] || false;
|
onClick.processing = true;
|
||||||
const isImage = el.type === 'radio';
|
(event.type.includes('Update') ? onUpdate() : onInstall())
|
||||||
if (v && (!isImage || el.checked)) {
|
.then(done, done);
|
||||||
const val = el.value;
|
function done() {
|
||||||
const isFile = val === 'user-upload';
|
setTimeout(() => {
|
||||||
if (isImage && (isFile || val === 'user-url')) {
|
onClick.processing = false;
|
||||||
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
|
});
|
||||||
const ikCust = `${ik}-custom`;
|
}
|
||||||
v.value = `${ikCust}-dropdown`;
|
}
|
||||||
vars[ikCust].value = isFile ? getFileUriFromPage(el2) : el2.value;
|
|
||||||
} else {
|
|
||||||
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
|
function onInstall() {
|
||||||
|
return getResource(getMeta('stylish-description'))
|
||||||
|
.then(name => saveStyleCode('styleInstall', name))
|
||||||
|
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onUpdate() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
method: 'getStyles',
|
||||||
|
md5Url: getMeta('stylish-md5-url') || location.href,
|
||||||
|
}, ([style]) => {
|
||||||
|
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
||||||
|
.then(resolve, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function saveStyleCode(message, name, addProps) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const isNew = message === 'styleInstall';
|
||||||
|
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
||||||
|
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveStyleCode.confirmed = true;
|
||||||
|
enableUpdateButton(false);
|
||||||
|
getStyleJson().then(json => {
|
||||||
|
if (!json) {
|
||||||
|
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||||
|
'https://github.com/openstyles/stylus/issues/195');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
Object.assign(json, addProps, {
|
||||||
|
method: 'saveStyle',
|
||||||
|
reason: isNew ? 'install' : 'update',
|
||||||
|
}),
|
||||||
|
style => {
|
||||||
|
if (!isNew && style.updateUrl.includes('?')) {
|
||||||
|
enableUpdateButton(true);
|
||||||
|
} else {
|
||||||
|
sendEvent('styleInstalledChrome');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function enableUpdateButton(state) {
|
||||||
|
const important = s => s.replace(/;/g, '!important;');
|
||||||
|
const button = document.getElementById('update_style_button');
|
||||||
|
if (button) {
|
||||||
|
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
|
||||||
|
const icon = button.querySelector('img[src*=".svg"]');
|
||||||
|
if (icon) {
|
||||||
|
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
|
||||||
|
if (state) {
|
||||||
|
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileUriFromPage(el) {
|
|
||||||
togglePageListener(true);
|
function getMeta(name) {
|
||||||
sendPageEvent(el);
|
const e = document.querySelector(`link[rel="${name}"]`);
|
||||||
return pageData;
|
return e ? e.getAttribute('href') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runInPage(fn, ...args) {
|
|
||||||
const div = document.createElement('div');
|
function getResource(url) {
|
||||||
div.attachShadow({mode: 'closed'})
|
return new Promise(resolve => {
|
||||||
.appendChild(document.createElement('script'))
|
if (url.startsWith('#')) {
|
||||||
.textContent = `(${fn})(${JSON.stringify(args).slice(1, -1)})`;
|
resolve(document.getElementById(url.slice(1)).textContent);
|
||||||
document.documentElement.appendChild(div).remove();
|
} else {
|
||||||
|
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPageEvent(data) {
|
|
||||||
dispatchEvent(data instanceof Node
|
function getStyleJson() {
|
||||||
? new MouseEvent(pageEventId, {relatedTarget: data})
|
const url = getStyleURL();
|
||||||
: new CustomEvent(pageEventId, {detail: data}));
|
return getResource(url).then(code => {
|
||||||
//* global cloneInto */// WARNING! Firefox requires cloning of an object `detail`
|
try {
|
||||||
|
return JSON.parse(code);
|
||||||
|
} catch (e) {
|
||||||
|
return fetch(url).then(r => r.json()).catch(() => null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendStylishEvent(type) {
|
|
||||||
document.dispatchEvent(new Event(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendVarsToPage(style) {
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
if (style) {
|
if (!a || !b) {
|
||||||
const vars = (style.usercssData || {}).vars || `${style.updateUrl}`.split('?')[1];
|
return undefined;
|
||||||
if (vars) sendPageEvent('vars:' + JSON.stringify(vars));
|
}
|
||||||
|
if (a.length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// order of sections should be identical to account for the case of multiple
|
||||||
|
// sections matching the same URL because the order of rules is part of cascading
|
||||||
|
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
||||||
|
|
||||||
|
function propertiesEqual(secA, secB) {
|
||||||
|
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||||
|
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalOrEmpty(a, b, telltale, comparator) {
|
||||||
|
const typeA = a && typeof a[telltale] === 'function';
|
||||||
|
const typeB = b && typeof b[telltale] === 'function';
|
||||||
|
return (
|
||||||
|
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||||
|
(b === null || b === undefined || (typeB && !b.length))
|
||||||
|
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayMirrors(array1, array2) {
|
||||||
|
return (
|
||||||
|
array1.every(el => array2.includes(el)) &&
|
||||||
|
array2.every(el => array1.includes(el))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return style || false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onDOMready() {
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
document.addEventListener('DOMContentLoaded', function _() {
|
||||||
|
document.removeEventListener('DOMContentLoaded', _);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function orphanCheck() {
|
function orphanCheck() {
|
||||||
if (chrome.runtime.id) return true;
|
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||||
removeEventListener(orphanEventId, orphanCheck, true);
|
return true;
|
||||||
removeEventListener('click', onClick, true);
|
}
|
||||||
removeEventListener('change', onChange);
|
// In Chrome content script is orphaned on an extension update/reload
|
||||||
sendPageEvent('quit');
|
// so we need to detach event listeners
|
||||||
observeColors(false);
|
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||||
togglePageListener(false);
|
['Update', 'Install'].forEach(type =>
|
||||||
|
['', 'Chrome', 'Opera'].forEach(browser =>
|
||||||
|
document.addEventListener('stylish' + type + browser, onClick)));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
|
// TODO: remove the following statement when USO is fixed
|
||||||
let done, orphaned, vars;
|
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
||||||
// `chrome` may be empty if no extensions use externally_connectable but USO needs it
|
function () {
|
||||||
if (!window.chrome) window.chrome = {runtime: {sendMessage: () => {}}};
|
let settings;
|
||||||
const EXT_ID = 'fjnbnpbmkenffdnngjfgmeleoegfcffe';
|
const originalResponseJson = Response.prototype.json;
|
||||||
const {defineProperty} = Object;
|
document.currentScript.remove();
|
||||||
const {dispatchEvent, CustomEvent, removeEventListener} = window;
|
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||||
const apply = Map.call.bind(Map.apply);
|
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||||
const OVR = [
|
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||||
[chrome.runtime, 'sendMessage', (fn, me, args) => {
|
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, ''));
|
||||||
const [id, /*msg*/, opts, cb = opts] = args;
|
if (!settings) {
|
||||||
if (id !== EXT_ID) return apply(fn, me, args);
|
Response.prototype.json = originalResponseJson;
|
||||||
if (typeof cb !== 'function') return Promise.resolve(true);
|
}
|
||||||
cb(true);
|
|
||||||
}],
|
|
||||||
[Response.prototype, 'json', async (fn, me, args) => {
|
|
||||||
const res = await apply(fn, me, args);
|
|
||||||
try {
|
|
||||||
if (!done && me.url === apiUrl) {
|
|
||||||
done = true;
|
|
||||||
send(res);
|
|
||||||
setVars(res);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
return res;
|
|
||||||
}],
|
|
||||||
[window, 'fetch', (fn, me, args) =>
|
|
||||||
args[0] === `chrome-extension://${EXT_ID}/index.html`
|
|
||||||
? Promise.resolve(new Response('<!doctype html><html lang="en"></html>'))
|
|
||||||
: apply(fn, me, args),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
OVR.forEach(([obj, name, caller], i) => {
|
|
||||||
const orig = obj[name];
|
|
||||||
const ovr = new Proxy(orig, {
|
|
||||||
apply(fn, me, args) {
|
|
||||||
if (orphaned) restore(obj, name, ovr, fn);
|
|
||||||
return (orphaned ? apply : caller)(fn, me, args);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
defineProperty(obj, name, {value: ovr});
|
Response.prototype.json = function (...args) {
|
||||||
OVR[i] = [obj, name, ovr, orig]; // same args as restore()
|
return originalResponseJson.call(this, ...args).then(json => {
|
||||||
});
|
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
|
||||||
/* We set `isInstalled` at page start intentionally not trying to replicate Stylish login events.
|
return json;
|
||||||
* This difference allows USO site to detect presence of Stylus (or another similar extension). */
|
|
||||||
window.isInstalled = true;
|
|
||||||
addEventListener(eventId, onCommand, true);
|
|
||||||
function onCommand(e) {
|
|
||||||
if (e.detail === 'quit') {
|
|
||||||
removeEventListener(eventId, onCommand, true);
|
|
||||||
OVR.forEach(restore);
|
|
||||||
done = orphaned = true;
|
|
||||||
} else if (/^vars:/.test(e.detail)) {
|
|
||||||
vars = JSON.parse(e.detail.slice(5));
|
|
||||||
} else if (e.relatedTarget) {
|
|
||||||
send(e.relatedTarget.uploadedData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function restore(obj, name, ovr, orig) { // same order as OVR after patching
|
|
||||||
if (obj[name] === ovr) {
|
|
||||||
defineProperty(obj, name, {value: orig});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function send(data) {
|
|
||||||
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
|
|
||||||
}
|
|
||||||
function setVars(json) {
|
|
||||||
const images = new Map();
|
|
||||||
const isNew = typeof vars === 'object';
|
|
||||||
const badKeys = {};
|
|
||||||
const newKeys = [];
|
|
||||||
const makeKey = ({install_key: key}) => {
|
|
||||||
let res = isNew ? badKeys[key] : key;
|
|
||||||
if (!res) {
|
|
||||||
res = key.replace(/[^-\w]/g, '-');
|
|
||||||
res += newKeys.includes(res) ? '-' : '';
|
|
||||||
if (key !== res) {
|
|
||||||
badKeys[key] = res;
|
|
||||||
newKeys.push(res);
|
|
||||||
}
|
}
|
||||||
}
|
Response.prototype.json = originalResponseJson;
|
||||||
return res;
|
const images = new Map();
|
||||||
};
|
for (const jsonSetting of json.style_settings) {
|
||||||
if (!isNew) vars = new URLSearchParams(vars);
|
let value = settings.get('ik-' + jsonSetting.install_key);
|
||||||
for (const ss of json.style_settings || []) {
|
if (!value
|
||||||
const ik = makeKey(ss);
|
|| !jsonSetting.style_setting_options
|
||||||
let value = isNew ? (vars[ik] || {}).value : vars.get('ik-' + ik);
|
|| !jsonSetting.style_setting_options[0]) {
|
||||||
if (value == null || !(ss.style_setting_options || [])[0]) {
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
if (value.startsWith('ik-')) {
|
||||||
if (ss.setting_type === 'image') {
|
value = value.replace(/^ik-/, '');
|
||||||
let isListed;
|
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
||||||
for (const opt of ss.style_setting_options) {
|
if (!defaultItem || defaultItem.install_key !== value) {
|
||||||
isListed |= opt.default = (opt.install_key === value);
|
if (defaultItem) {
|
||||||
}
|
defaultItem.default = false;
|
||||||
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
|
}
|
||||||
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {
|
jsonSetting.style_setting_options.some(item => {
|
||||||
value = value.replace(/^ik-/, '');
|
if (item.install_key === value) {
|
||||||
const def = ss.style_setting_options.find(item => item.default);
|
item.default = true;
|
||||||
if (!def || makeKey(def) !== value) {
|
return true;
|
||||||
if (def) def.default = false;
|
}
|
||||||
for (const item of ss.style_setting_options) {
|
});
|
||||||
if (makeKey(item) === value) {
|
}
|
||||||
item.default = true;
|
} else if (jsonSetting.setting_type === 'image') {
|
||||||
break;
|
jsonSetting.style_setting_options.some(item => {
|
||||||
|
if (item.default) {
|
||||||
|
item.default = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
images.set(jsonSetting.install_key, value);
|
||||||
|
} else {
|
||||||
|
const item = jsonSetting.style_setting_options[0];
|
||||||
|
if (item.value !== value && item.install_key === 'placeholder') {
|
||||||
|
item.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
if (images.size) {
|
||||||
const item = ss.style_setting_options[0];
|
new MutationObserver((_, observer) => {
|
||||||
if (item.value !== value && item.install_key === 'placeholder') {
|
if (!document.getElementById('style-settings')) {
|
||||||
item.value = value;
|
return;
|
||||||
|
}
|
||||||
|
observer.disconnect();
|
||||||
|
for (const [name, url] of images.entries()) {
|
||||||
|
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||||
|
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||||
|
if (elUrl) {
|
||||||
|
elUrl.value = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).observe(document, {childList: true, subtree: true});
|
||||||
}
|
}
|
||||||
}
|
return json;
|
||||||
}
|
});
|
||||||
if (!images.size) return;
|
};
|
||||||
|
} + ')()';
|
||||||
|
|
||||||
|
// TODO: remove the following statement when USO pagination is fixed
|
||||||
|
if (location.search.includes('category=')) {
|
||||||
|
document.addEventListener('DOMContentLoaded', function _() {
|
||||||
|
document.removeEventListener('DOMContentLoaded', _);
|
||||||
new MutationObserver((_, observer) => {
|
new MutationObserver((_, observer) => {
|
||||||
if (!document.getElementById('style-settings')) return;
|
if (!document.getElementById('pagination')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
for (const [name, {url, isListed}] of images) {
|
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
||||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
||||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
for (let i = 0; i < links.length; i++) {
|
||||||
if (elUrl) {
|
links[i].href += category;
|
||||||
elRadio.checked = !isListed;
|
|
||||||
elUrl.value = url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).observe(document, {childList: true, subtree: true});
|
}).observe(document, {childList: true, subtree: true});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
/* global API */// msg.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const ORIGIN = 'https://userstyles.world';
|
|
||||||
const HANDLERS = Object.assign(Object.create(null), {
|
|
||||||
|
|
||||||
async 'usw-ready'() {
|
|
||||||
send({type: 'usw-remove-stylus-button'});
|
|
||||||
if (location.pathname === '/api/oauth/style/new') {
|
|
||||||
const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
|
|
||||||
const data = await API.data.pop('usw' + styleId);
|
|
||||||
send({type: 'usw-fill-new-style', data});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async 'usw-style-info-request'(data) {
|
|
||||||
switch (data.requestType) {
|
|
||||||
case 'installed': {
|
|
||||||
const updateUrl = `${ORIGIN}/api/style/${data.styleID}.user.css`;
|
|
||||||
const style = await API.styles.find({updateUrl});
|
|
||||||
send({
|
|
||||||
type: 'usw-style-info-response',
|
|
||||||
data: {installed: Boolean(style), requestType: 'installed'},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('message', ({data, source, origin}) => {
|
|
||||||
// Some browsers don't reveal `source` to extensions e.g. Firefox
|
|
||||||
if (data && (source ? source === window : origin === ORIGIN)) {
|
|
||||||
const fn = HANDLERS[data.type];
|
|
||||||
if (fn) fn(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function send(msg) {
|
|
||||||
window.postMessage(msg, ORIGIN);
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,345 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {function(opts):StyleInjector} */
|
|
||||||
window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
|
|
||||||
compare,
|
|
||||||
onUpdate = () => {},
|
|
||||||
}) => {
|
|
||||||
const PREFIX = 'stylus-';
|
|
||||||
const PATCH_ID = 'transition-patch';
|
|
||||||
// styles are out of order if any of these elements is injected between them
|
|
||||||
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
|
|
||||||
const docRewriteObserver = RewriteObserver(sort);
|
|
||||||
const docRootObserver = RootObserver(sortIfNeeded);
|
|
||||||
const toSafeChar = c => String.fromCharCode(0xFF00 + c.charCodeAt(0) - 0x20);
|
|
||||||
const list = [];
|
|
||||||
const table = new Map();
|
|
||||||
let isEnabled = true;
|
|
||||||
let isTransitionPatched = chrome.app && CSS.supports('accent-color', 'red'); // Chrome 93
|
|
||||||
let exposeStyleName;
|
|
||||||
// will store the original method refs because the page can override them
|
|
||||||
let creationDoc, createElement, createElementNS;
|
|
||||||
|
|
||||||
return /** @namespace StyleInjector */ {
|
|
||||||
|
|
||||||
list,
|
|
||||||
|
|
||||||
async apply(styleMap) {
|
|
||||||
const styles = styleMapToArray(styleMap);
|
|
||||||
const value = !styles.length
|
|
||||||
? []
|
|
||||||
: await docRootObserver.evade(() => {
|
|
||||||
if (!isTransitionPatched && isEnabled) {
|
|
||||||
applyTransitionPatch(styles);
|
|
||||||
}
|
|
||||||
return styles.map(addUpdate);
|
|
||||||
});
|
|
||||||
emitUpdate();
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
addRemoveElements(false);
|
|
||||||
list.length = 0;
|
|
||||||
table.clear();
|
|
||||||
emitUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
clearOrphans() {
|
|
||||||
for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) {
|
|
||||||
const id = el.id.slice(PREFIX.length);
|
|
||||||
if (/^\d+$/.test(id) || id === PATCH_ID) {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(id) {
|
|
||||||
remove(id);
|
|
||||||
emitUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
replace(styleMap) {
|
|
||||||
const styles = styleMapToArray(styleMap);
|
|
||||||
const added = new Set(styles.map(s => s.id));
|
|
||||||
const removed = [];
|
|
||||||
for (const style of list) {
|
|
||||||
if (!added.has(style.id)) {
|
|
||||||
removed.push(style.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styles.forEach(addUpdate);
|
|
||||||
removed.forEach(remove);
|
|
||||||
emitUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
toggle(enable) {
|
|
||||||
if (isEnabled === enable) return;
|
|
||||||
isEnabled = enable;
|
|
||||||
if (!enable) toggleObservers(false);
|
|
||||||
addRemoveElements(enable);
|
|
||||||
if (enable) toggleObservers(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
sort: sort,
|
|
||||||
};
|
|
||||||
|
|
||||||
function add(style) {
|
|
||||||
const el = style.el = createStyle(style);
|
|
||||||
const i = list.findIndex(item => compare(item, style) > 0);
|
|
||||||
table.set(style.id, style);
|
|
||||||
if (isEnabled) {
|
|
||||||
document.documentElement.insertBefore(el, i < 0 ? null : list[i].el);
|
|
||||||
}
|
|
||||||
list.splice(i < 0 ? list.length : i, 0, style);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRemoveElements(add) {
|
|
||||||
for (const {el} of list) {
|
|
||||||
if (add) {
|
|
||||||
document.documentElement.appendChild(el);
|
|
||||||
} else {
|
|
||||||
el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addUpdate(style) {
|
|
||||||
return table.has(style.id) ? update(style) : add(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTransitionPatch(styles) {
|
|
||||||
isTransitionPatched = true;
|
|
||||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
|
||||||
// the browsers, especially Firefox, may apply all transitions on page load
|
|
||||||
if (document.readyState === 'complete' ||
|
|
||||||
document.visibilityState === 'hidden' ||
|
|
||||||
!styles.some(s => s.code.includes('transition'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const el = createStyle({id: PATCH_ID, code: `
|
|
||||||
:root:not(#\\0):not(#\\0) * {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
`});
|
|
||||||
document.documentElement.appendChild(el);
|
|
||||||
// wait for the next paint to complete
|
|
||||||
// note: requestAnimationFrame won't fire in inactive tabs
|
|
||||||
requestAnimationFrame(() => setTimeout(() => el.remove()));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStyle(style = {}) {
|
|
||||||
const {id} = style;
|
|
||||||
if (!creationDoc) initCreationDoc();
|
|
||||||
let el;
|
|
||||||
if (document.documentElement instanceof SVGSVGElement) {
|
|
||||||
// SVG document style
|
|
||||||
el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style');
|
|
||||||
} else if (document instanceof XMLDocument) {
|
|
||||||
// XML document style
|
|
||||||
el = createElementNS.call(creationDoc, 'http://www.w3.org/1999/xhtml', 'style');
|
|
||||||
} else {
|
|
||||||
// HTML document style; also works on HTML-embedded SVG
|
|
||||||
el = createElement.call(creationDoc, 'style');
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
el.id = `${PREFIX}${id}`;
|
|
||||||
const oldEl = document.getElementById(el.id);
|
|
||||||
if (oldEl) oldEl.id += '-superseded-by-Stylus';
|
|
||||||
}
|
|
||||||
el.type = 'text/css';
|
|
||||||
// SVG className is not a string, but an instance of SVGAnimatedString
|
|
||||||
el.classList.add('stylus');
|
|
||||||
setTextAndName(el, style);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTextAndName(el, {id, code = '', name}) {
|
|
||||||
if (exposeStyleName && name) {
|
|
||||||
el.dataset.name = name;
|
|
||||||
name = encodeURIComponent(name.replace(/[?#/']/g, toSafeChar));
|
|
||||||
code += `\n/*# sourceURL=${chrome.runtime.getURL(name)}.user.css#${id} */`;
|
|
||||||
}
|
|
||||||
el.textContent = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleObservers(shouldStart) {
|
|
||||||
const onOff = shouldStart && isEnabled ? 'start' : 'stop';
|
|
||||||
docRewriteObserver[onOff]();
|
|
||||||
docRootObserver[onOff]();
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitUpdate() {
|
|
||||||
toggleObservers(list.length);
|
|
||||||
onUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461
|
|
||||||
First we're trying the page context document where inline styles may be forbidden by CSP
|
|
||||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1579345#c3
|
|
||||||
and since userAgent.navigator can be spoofed via about:config or devtools,
|
|
||||||
we're checking for getPreventDefault that was removed in FF59
|
|
||||||
*/
|
|
||||||
function initCreationDoc() {
|
|
||||||
creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
|
|
||||||
if (creationDoc) {
|
|
||||||
({createElement, createElementNS} = creationDoc);
|
|
||||||
const el = document.documentElement.appendChild(createStyle());
|
|
||||||
const isApplied = el.sheet;
|
|
||||||
el.remove();
|
|
||||||
if (isApplied) return;
|
|
||||||
}
|
|
||||||
creationDoc = document;
|
|
||||||
({createElement, createElementNS} = document);
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(id) {
|
|
||||||
const style = table.get(id);
|
|
||||||
if (!style) return;
|
|
||||||
table.delete(id);
|
|
||||||
list.splice(list.indexOf(style), 1);
|
|
||||||
style.el.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sort() {
|
|
||||||
docRootObserver.evade(() => {
|
|
||||||
list.sort(compare);
|
|
||||||
addRemoveElements(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortIfNeeded() {
|
|
||||||
let needsSort;
|
|
||||||
let el = list.length && list[0].el;
|
|
||||||
if (!el) {
|
|
||||||
needsSort = false;
|
|
||||||
} else if (el.parentNode !== creationDoc.documentElement) {
|
|
||||||
needsSort = true;
|
|
||||||
} else {
|
|
||||||
let i = 0;
|
|
||||||
while (el) {
|
|
||||||
if (i < list.length && el === list[i].el) {
|
|
||||||
i++;
|
|
||||||
} else if (ORDERED_TAGS.has(el.localName)) {
|
|
||||||
needsSort = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
el = el.nextElementSibling;
|
|
||||||
}
|
|
||||||
// some styles are not injected to the document
|
|
||||||
if (i < list.length) needsSort = true;
|
|
||||||
}
|
|
||||||
if (needsSort) sort();
|
|
||||||
return needsSort;
|
|
||||||
}
|
|
||||||
|
|
||||||
function styleMapToArray(styleMap) {
|
|
||||||
if (styleMap.cfg) {
|
|
||||||
({exposeStyleName} = styleMap.cfg);
|
|
||||||
delete styleMap.cfg;
|
|
||||||
}
|
|
||||||
return Object.values(styleMap).map(({id, code, name}) => ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
code: code.join(''),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(newStyle) {
|
|
||||||
const {id, code} = newStyle;
|
|
||||||
const style = table.get(id);
|
|
||||||
if (style.code !== code ||
|
|
||||||
style.name !== newStyle.name && exposeStyleName) {
|
|
||||||
style.code = code;
|
|
||||||
setTextAndName(style.el, newStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RewriteObserver(onChange) {
|
|
||||||
// detect documentElement being rewritten from inside the script
|
|
||||||
let root;
|
|
||||||
let observing = false;
|
|
||||||
let timer;
|
|
||||||
const observer = new MutationObserver(check);
|
|
||||||
return {start, stop};
|
|
||||||
|
|
||||||
function start() {
|
|
||||||
if (observing) return;
|
|
||||||
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
|
||||||
root = document.documentElement;
|
|
||||||
timer = setTimeout(check);
|
|
||||||
observer.observe(document, {childList: true});
|
|
||||||
observing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop() {
|
|
||||||
if (!observing) return;
|
|
||||||
clearTimeout(timer);
|
|
||||||
observer.disconnect();
|
|
||||||
observing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function check() {
|
|
||||||
if (root !== document.documentElement) {
|
|
||||||
root = document.documentElement;
|
|
||||||
onChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RootObserver(onChange) {
|
|
||||||
let digest = 0;
|
|
||||||
let lastCalledTime = NaN;
|
|
||||||
let observing = false;
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
if (digest) {
|
|
||||||
if (performance.now() - lastCalledTime > 1000) {
|
|
||||||
digest = 0;
|
|
||||||
} else if (digest > 5) {
|
|
||||||
throw new Error('The page keeps generating mutations. Skip the event.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (onChange()) {
|
|
||||||
digest++;
|
|
||||||
lastCalledTime = performance.now();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {evade, start, stop};
|
|
||||||
|
|
||||||
function evade(fn) {
|
|
||||||
const restore = observing && start;
|
|
||||||
stop();
|
|
||||||
return new Promise(resolve => run(fn, resolve, waitForRoot))
|
|
||||||
.then(restore);
|
|
||||||
}
|
|
||||||
|
|
||||||
function start() {
|
|
||||||
if (observing) return;
|
|
||||||
observer.observe(document.documentElement, {childList: true});
|
|
||||||
observing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop() {
|
|
||||||
if (!observing) return;
|
|
||||||
// FIXME: do we need this?
|
|
||||||
observer.takeRecords();
|
|
||||||
observer.disconnect();
|
|
||||||
observing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function run(fn, resolve, wait) {
|
|
||||||
if (document.documentElement) {
|
|
||||||
resolve(fn());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (wait) wait(fn, resolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForRoot(...args) {
|
|
||||||
new MutationObserver((_, observer) => run(...args) && observer.disconnect())
|
|
||||||
.observe(document, {childList: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
601
edit.html
601
edit.html
|
@ -1,209 +1,155 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html id="stylus">
|
<html id="stylus">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<link href="global.css" rel="stylesheet">
|
<link rel="stylesheet" href="global.css">
|
||||||
<link href="global-dark.css" rel="stylesheet">
|
<link rel="stylesheet" href="edit/edit.css">
|
||||||
<style id="cm-theme"></style>
|
|
||||||
|
<style id="firefox-transitions-bug-suppressor">
|
||||||
|
/* restrict to FF */
|
||||||
|
@supports (-moz-appearance:none) {
|
||||||
|
/* increased specificity to override sane selectors in user styles */
|
||||||
|
html#stylus.firefox #stylus-edit #header *,
|
||||||
|
html#stylus.firefox #stylus-edit #sections * {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script src="js/polyfill.js"></script>
|
|
||||||
<script src="js/toolbox.js"></script>
|
|
||||||
<script src="js/msg.js"></script>
|
|
||||||
<script src="js/prefs.js"></script>
|
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/dom.js"></script>
|
||||||
|
<script src="js/messaging.js"></script>
|
||||||
|
<script src="js/prefs.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
<script src="content/style-injector.js"></script>
|
<script src="js/script-loader.js"></script>
|
||||||
|
<script src="js/moz-parser.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
|
<script src="edit/lint.js"></script>
|
||||||
<script src="js/sections-util.js"></script>
|
<script src="edit/util.js"></script>
|
||||||
<script src="js/storage-util.js"></script>
|
<script src="edit/regexp-tester.js"></script>
|
||||||
<script src="edit/codemirror-themes.js"></script> <!-- must precede base.js -->
|
<script src="edit/applies-to-line-widget.js"></script>
|
||||||
<script src="edit/base.js"></script>
|
<script src="edit/source-editor.js"></script>
|
||||||
|
<script src="edit/colorpicker-helper.js"></script>
|
||||||
|
<script src="edit/beautify.js"></script>
|
||||||
|
<script src="edit/sections.js"></script>
|
||||||
|
<script src="edit/show-keymap-help.js"></script>
|
||||||
|
<script src="edit/codemirror-editing-hooks.js"></script>
|
||||||
|
<script src="edit/edit.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||||
|
<link rel="stylesheet" href="vendor/codemirror/lib/codemirror.css">
|
||||||
<script src="vendor/codemirror/mode/css/css.js"></script>
|
<script src="vendor/codemirror/mode/css/css.js"></script>
|
||||||
<script src="vendor/codemirror/mode/stylus/stylus.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
<link rel="stylesheet" href="vendor/codemirror/addon/dialog/dialog.css">
|
||||||
<script src="vendor/codemirror/addon/edit/closebrackets.js"></script>
|
<link rel="stylesheet" href="vendor/codemirror/addon/search/matchesonscrollbar.css">
|
||||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/search/match-highlighter.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/search/search.js"></script>
|
||||||
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
||||||
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
||||||
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
|
||||||
|
<link rel="stylesheet" href="vendor/codemirror/addon/fold/foldgutter.css" />
|
||||||
<script src="vendor/codemirror/addon/fold/foldcode.js"></script>
|
<script src="vendor/codemirror/addon/fold/foldcode.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/indent-fold.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||||
<script src="vendor/codemirror/addon/lint/lint.js"></script>
|
|
||||||
|
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="vendor/codemirror/addon/lint/lint.css" />
|
||||||
|
<link rel="stylesheet" href="vendor/codemirror/addon/hint/show-hint.css" />
|
||||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||||
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
|
||||||
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
||||||
|
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
<script src="vendor/codemirror/keymap/vim.js"></script>
|
||||||
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
|
|
||||||
|
|
||||||
<script src="js/color/color-converter.js"></script>
|
<link href="/vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
|
||||||
<script src="js/color/color-mimicry.js"></script>
|
<script src="/vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||||
<script src="js/color/color-picker.js"></script>
|
<script src="/vendor-overwrites/colorpicker/colorview.js"></script>
|
||||||
<script src="js/color/color-view.js"></script>
|
|
||||||
<script src="js/worker-util.js"></script>
|
|
||||||
|
|
||||||
<script src="edit/util.js"></script>
|
<script src="edit/match-highlighter-helper.js"></script>
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
<script src="edit/codemirror-factory.js"></script>
|
|
||||||
<script src="edit/moz-section-finder.js"></script>
|
<link rel="stylesheet" href="/edit/codemirror-default.css">
|
||||||
<script src="edit/moz-section-widget.js"></script>
|
|
||||||
<script src="edit/linter-manager.js"></script>
|
|
||||||
<script src="edit/beautify.js"></script>
|
|
||||||
<script src="edit/source-editor.js"></script>
|
|
||||||
<script src="edit/sections-editor-section.js"></script>
|
|
||||||
<script src="edit/sections-editor.js"></script>
|
|
||||||
<script src="edit/usw-integration.js"></script>
|
|
||||||
|
|
||||||
<template data-id="appliesTo">
|
<template data-id="appliesTo">
|
||||||
<li class="applies-to-item">
|
<li class="applies-to-item">
|
||||||
<div class="select-resizer">
|
<div class="select-resizer">
|
||||||
<select name="applies-type" class="applies-type style-contributor">
|
<select name="applies-type" class="applies-type style-contributor">
|
||||||
<option value="url" i18n="appliesUrlOption"></option>
|
<option value="url" i18n-text="appliesUrlOption"></option>
|
||||||
<option value="url-prefix" i18n="appliesUrlPrefixOption"></option>
|
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
|
||||||
<option value="domain" i18n="appliesDomainOption"></option>
|
<option value="domain" i18n-text="appliesDomainOption"></option>
|
||||||
<option value="regexp" i18n="appliesRegexpOption"></option>
|
<option value="regexp" i18n-text="appliesRegexpOption"></option>
|
||||||
</select>
|
</select>
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="applies-value-wrapper">
|
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
|
||||||
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
|
<button class="remove-applies-to" i18n-text="appliesRemove"></button>
|
||||||
<a class="remove-applies-to" i18n="appliesRemove, title:appliesRemove" tabindex="0">
|
<button class="add-applies-to" i18n-text="appliesAdd"></button>
|
||||||
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
|
|
||||||
</a>
|
|
||||||
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
|
|
||||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="appliesToEverything">
|
<template data-id="appliesToEverything">
|
||||||
<li class="applies-to-everything" i18n="appliesToEverything">
|
<li class="applies-to-everything" i18n-text="appliesToEverything">
|
||||||
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
|
<button class="add-applies-to" i18n-text="appliesSpecify"></button>
|
||||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="section">
|
<template data-id="section">
|
||||||
<div class="section">
|
<div>
|
||||||
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
|
<label i18n-text="sectionCode" class="code-label"></label>
|
||||||
<p class="deleted-section">
|
<br>
|
||||||
<button class="restore-section" i18n="sectionRestore"></button>
|
|
||||||
</p>
|
|
||||||
<label i18n="sectionCode" class="code-label"></label>
|
|
||||||
<div class="applies-to">
|
<div class="applies-to">
|
||||||
<label i18n="appliesLabel, title:appliesHelp" data-cmd="note">
|
<label i18n-text="appliesLabel">
|
||||||
<a class="svg-inline-wrapper applies-to-help" tabindex="0">
|
<svg class="svg-icon info applies-to-help"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
|
||||||
</a>
|
|
||||||
</label>
|
</label>
|
||||||
<ul class="applies-to-list"></ul>
|
<ul class="applies-to-list"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions">
|
<button class="remove-section" i18n-text="sectionRemove"></button>
|
||||||
<button class="remove-section" i18n="sectionRemove"></button>
|
<button class="add-section" i18n-text="sectionAdd"></button>
|
||||||
<button class="add-section" i18n="long-text:sectionAdd, short-text:genericAdd"></button>
|
<button class="beautify-section" i18n-text="styleBeautify"></button>
|
||||||
<button class="clone-section" i18n="genericClone"></button>
|
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
|
||||||
<button class="move-section-up"></button>
|
|
||||||
<button class="move-section-down"></button>
|
|
||||||
<button class="beautify-section" i18n="styleBeautify"></button>
|
|
||||||
<button class="test-regexp" i18n="genericTest"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template data-id="searchReplaceDialog">
|
|
||||||
<div id="search-replace-dialog">
|
|
||||||
<div data-type="main">
|
|
||||||
<div data-type="content"></div>
|
|
||||||
<div data-type="actions">
|
|
||||||
<a data-action="case" i18n="title:searchCaseSensitive" tabindex="0">Aa</a>
|
|
||||||
<a data-action="prev" i18n="title:genericPrevious" data-hotkey-tooltip="findPrev" tabindex="0">
|
|
||||||
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
|
|
||||||
</a>
|
|
||||||
<a data-action="next" i18n="title:genericNext" data-hotkey-tooltip="findNext" tabindex="0">
|
|
||||||
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
|
|
||||||
</a>
|
|
||||||
<a data-action="close" i18n="title:confirmClose" data-hotkey-tooltip="=Esc" tabindex="0">
|
|
||||||
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-type="status">
|
|
||||||
<div class="CodeMirror-search-hint" i18n-text="searchRegexp"></div>
|
|
||||||
<div data-type="tally" i18n="title:searchNumberOfResults"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template data-id="clearSearch">
|
|
||||||
<div data-type="hover" i18n="title:confirmDelete">
|
|
||||||
<svg data-action="clear" class="svg-icon"><use xlink:href="#svg-icon-close"></use></svg>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="find">
|
<template data-id="find">
|
||||||
<div data-type="content">
|
<span i18n-text="search">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||||
<div data-type="input-wrapper">
|
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||||
<textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required
|
</span>
|
||||||
i18n="placeholder:search"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="replace">
|
<template data-id="replace">
|
||||||
<div data-type="content">
|
<span i18n-text="replace">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||||
<div data-type="input-wrapper">
|
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||||
<textarea data-type="replace-from"
|
</span>
|
||||||
i18n="placeholder:replace"
|
</template>
|
||||||
class="CodeMirror-search-field" rows="1" required
|
|
||||||
spellcheck="false"></textarea>
|
<template data-id="replaceAll">
|
||||||
</div>
|
<span i18n-text="replaceAll">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||||
<div data-type="input-wrapper">
|
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||||
<textarea data-type="replace-to"
|
</span>
|
||||||
i18n="placeholder:replaceWith"
|
</template>
|
||||||
class="CodeMirror-search-field" rows="1" required
|
|
||||||
spellcheck="false"></textarea>
|
<template data-id="replaceWith">
|
||||||
</div>
|
<span i18n-text="replaceWith">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||||
<button data-action="replace" i18n="replace" disabled></button>
|
</span>
|
||||||
<button data-action="replaceAll" i18n="replaceAll" disabled></button>
|
</template>
|
||||||
<button data-action="undo" i18n="undo" disabled></button>
|
|
||||||
<!--
|
<template data-id="replaceConfirm">
|
||||||
Using a separate set of buttons because
|
<span i18n-text="replace">?
|
||||||
1. FF can display tooltips only when specified on the <button>, ignores the nested <title> in <svg>
|
<button i18n-text="confirmYes"></button>
|
||||||
2. the icon doesn't fill the entire button area so tooltips aren't shown when the edges are hovered
|
<button i18n-text="confirmNo"></button>
|
||||||
-->
|
<button i18n-text="confirmStop"></button>
|
||||||
<button class="hidden" data-action="replace" i18n="title:replace" disabled>
|
</span>
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
|
||||||
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="hidden" data-action="replaceAll" i18n="title:replaceAll" disabled>
|
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
|
||||||
<polygon points="15.8,1.8 8.8,8.8 5.2,5.3 3.5,6.9 8.8,12.2 17.5,3.4 "/>
|
|
||||||
<polygon points="15.8,7.8 8.8,14.8 5.2,11.3 3.5,12.9 8.8,18.2 17.5,9.4 "/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="hidden" data-action="undo" i18n="title:undo" disabled>
|
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
|
||||||
<path d="M11.3,5.5H8.7V1.4L1.9,6.5l6.8,5.1V7.5h2.6c1.8,0,3.2,1.4,3.2,3.2s-1.4,3.2-3.2,3.2H7.8v2h3.5c2.9,0,5.2-2.3,5.2-5.2S14.2,5.5,11.3,5.5z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="jumpToLine">
|
<template data-id="jumpToLine">
|
||||||
<span i18n="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="regexpTestPartial">
|
<template data-id="regexpTestPartial">
|
||||||
|
@ -211,15 +157,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="resizeGrip">
|
<template data-id="resizeGrip">
|
||||||
<div class="resize-grip" i18n="title:cm_resizeGripHint"></div>
|
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="keymapHelp">
|
<template data-id="keymapHelp">
|
||||||
<table class="keymap-list">
|
<table class="keymap-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><input i18n="placeholder:helpKeyMapHotkey" type="search"></th>
|
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th>
|
||||||
<th><input i18n="placeholder:helpKeyMapCommand" type="search"></th>
|
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -230,225 +176,155 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
</head>
|
||||||
|
|
||||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
<body id="stylus-edit">
|
||||||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
|
||||||
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet">
|
|
||||||
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet">
|
|
||||||
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet">
|
|
||||||
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
|
||||||
<link href="js/color/color-picker.css" rel="stylesheet">
|
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<template data-id="body"> <!-- https://crbug.com/1288447 -->
|
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<h1 id="heading" i18n="data-edit:editStyleHeading, data-add:addStyleTitle">
|
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||||
<a class="usercss-only"
|
|
||||||
href="https://github.com/openstyles/stylus/wiki/Usercss"
|
|
||||||
i18n="title:externalUsercssDocument" target="_blank">
|
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<section id="basic-info">
|
<section id="basic-info">
|
||||||
<div id="basic-info-name">
|
<div id="basic-info-name">
|
||||||
<input id="name" class="style-contributor" spellcheck="false">
|
<input id="name" class="style-contributor" spellcheck="false">
|
||||||
<a id="reset-name" i18n="title:customNameResetHint" tabindex="0" hidden>
|
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
|
||||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
|
||||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="basic-info-enabled">
|
<div id="basic-info-enabled">
|
||||||
<label id="enabled-label"
|
<label id="enabled-label" i18n-text="styleEnabledLabel">
|
||||||
i18n="styleEnabledLabel, title:toggleStyle"
|
|
||||||
data-hotkey-tooltip="toggleStyle">
|
|
||||||
<input type="checkbox" id="enabled" class="style-contributor">
|
<input type="checkbox" id="enabled" class="style-contributor">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
</label>
|
</label><!--
|
||||||
<label id="preview-label" i18n="previewLabel, title:previewTooltip">
|
--><svg id="toggle-style-help" class="svg-icon info">
|
||||||
<input type="checkbox" id="editor.livePreview">
|
<use xlink:href="#svg-icon-help"/>
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
</svg>
|
||||||
</label>
|
|
||||||
<label id="disableAll-label" i18n="data-on:disableAllStyles, data-off:disableAllStylesOff">
|
|
||||||
<input id="disableAll" type="checkbox">
|
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
|
||||||
</label>
|
|
||||||
<span id="preview-errors" hidden>!</span>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="actions">
|
<section id="actions">
|
||||||
<div class="buttons">
|
<div>
|
||||||
<div class="split-btn">
|
<button id="save-button" i18n-text="styleSaveLabel"></button>
|
||||||
<button id="save-button" i18n="styleSaveLabel" data-hotkey-tooltip="save" disabled></button
|
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||||
><button class="split-btn-pedal usercss-only" i18n="menu-tpl:saveAsTemplate"></button>
|
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||||
</div>
|
|
||||||
<button id="beautify" i18n="styleBeautify"></button>
|
|
||||||
<button id="style-settings-btn" i18n="settings"></button>
|
|
||||||
<button id="cancel-button" i18n="title:styleCancelEditLabel">↩</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="mozilla-format-buttons" class="buttons sectioned-only">
|
<div id="mozilla-format-container">
|
||||||
<button id="from-mozilla" i18n="importLabel"></button>
|
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||||
<button id="to-mozilla" i18n="exportLabel"></button>
|
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||||
<a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0"
|
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||||
i18n="title:styleMozillaFormatHeading">
|
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div id="details-wrapper">
|
<details id="options" data-pref="editor.options.expanded">
|
||||||
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
|
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
||||||
<summary><h2 id="options-heading" i18n="editorSettings"></h2></summary>
|
<div class="option">
|
||||||
<div id="options-wrapper">
|
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
|
||||||
<div class="options-column">
|
<input id="editor.lineWrapping" type="checkbox">
|
||||||
<div class="option">
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<label id="lineWrapping-label" i18n="cm_lineWrapping">
|
</label>
|
||||||
<input id="editor.lineWrapping" type="checkbox">
|
</div>
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<div class="option">
|
||||||
</label>
|
<label id="smartIndent-label" i18n-text="cm_smartIndent">
|
||||||
</div>
|
<input id="editor.smartIndent" type="checkbox">
|
||||||
<div class="option">
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<label id="smartIndent-label" i18n="cm_smartIndent">
|
</label>
|
||||||
<input id="editor.smartIndent" type="checkbox">
|
</div>
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<div class="option">
|
||||||
</label>
|
<label id="indentWithTabs-label" i18n-text="cm_indentWithTabs">
|
||||||
</div>
|
<input id="editor.indentWithTabs" type="checkbox">
|
||||||
<div class="option">
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<label id="indentWithTabs-label" i18n="cm_indentWithTabs">
|
</label>
|
||||||
<input id="editor.indentWithTabs" type="checkbox">
|
</div>
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<div class="option">
|
||||||
</label>
|
<label i18n-text="cm_autoCloseBrackets" i18n-title="cm_autoCloseBracketsTooltip">
|
||||||
</div>
|
<input id="editor.autoCloseBrackets" type="checkbox">
|
||||||
<div class="option">
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<label i18n="cm_autoCloseBrackets, title:cm_autoCloseBracketsTooltip">
|
</label>
|
||||||
<input id="editor.autoCloseBrackets" type="checkbox">
|
</div>
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<div class="option">
|
||||||
</label>
|
<label i18n-text="cm_autocompleteOnTyping">
|
||||||
</div>
|
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||||
<div class="option">
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<label i18n="cm_autocompleteOnTyping">
|
</label>
|
||||||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
</div>
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<div class="option">
|
||||||
</label>
|
<label i18n-text="cm_colorpicker">
|
||||||
</div>
|
<input id="editor.colorpicker" type="checkbox">
|
||||||
<div class="option">
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<label i18n="cm_selectByTokens, title:cm_selectByTokensTooltip">
|
</label>
|
||||||
<input id="editor.selectByTokens" type="checkbox">
|
<span class="svg-inline-wrapper" i18n-title="shortcutsNote">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<svg id="colorpicker-settings" class="svg-icon settings">
|
||||||
</label>
|
<use xlink:href="#svg-icon-settings"/>
|
||||||
</div>
|
</svg>
|
||||||
<div class="option sectioned-only">
|
</span>
|
||||||
<label i18n="cm_arrowKeysTraverse">
|
</div>
|
||||||
<input id="editor.arrowKeysTraverse" type="checkbox">
|
<div class="option usercss-only">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<label i18n-text="appliesLineWidgetLabel" i18n-title="appliesLineWidgetWarning">
|
||||||
</label>
|
<input id="editor.appliesToLineWidget" type="checkbox">
|
||||||
</div>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<div class="option">
|
</label>
|
||||||
<label i18n="cm_colorpicker">
|
</div>
|
||||||
<input id="editor.colorpicker" type="checkbox">
|
<div class="option aligned">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||||
</label>
|
<input id="editor.tabSize" type="number" min="0">
|
||||||
<a id="colorpicker-settings" class="svg-inline-wrapper" i18n="title:shortcutsNote" tabindex="0">
|
</div>
|
||||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
|
<div class="option aligned">
|
||||||
</a>
|
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||||
</div>
|
<div class="select-resizer">
|
||||||
<div class="option usercss-only">
|
<select id="editor.keyMap"></select>
|
||||||
<label i18n="appliesLineWidgetLabel, title:appliesLineWidgetWarning">
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
<input id="editor.appliesToLineWidget" type="checkbox">
|
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="options-column">
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="tabSize-label" for="editor.tabSize" i18n="cm_tabSize"></label>
|
|
||||||
<input id="editor.tabSize" type="number" min="0">
|
|
||||||
</div>
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="keyMap-label" for="editor.keyMap" i18n="cm_keyMap"></label>
|
|
||||||
<div class="select-resizer">
|
|
||||||
<select id="editor.keyMap"></select>
|
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
|
||||||
<a id="keyMap-help" class="svg-inline-wrapper" tabindex="0">
|
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="theme-label" for="editor.theme" i18n="cm_theme"></label>
|
|
||||||
<div class="select-resizer">
|
|
||||||
<select id="editor.theme"></select>
|
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="highlight-label" for="editor.matchHighlight" i18n="cm_matchHighlight"></label>
|
|
||||||
<div class="select-resizer">
|
|
||||||
<select id="editor.matchHighlight">
|
|
||||||
<option i18n="cm_matchHighlightToken" value="token">
|
|
||||||
<option i18n="cm_matchHighlightSelection" value="selection">
|
|
||||||
<option i18n="genericDisabledLabel" value="">
|
|
||||||
</select>
|
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="option aligned">
|
|
||||||
<label id="linter-label" for="editor.linter" i18n="cm_linter"></label>
|
|
||||||
<div class="select-resizer">
|
|
||||||
<select id="editor.linter">
|
|
||||||
<option value="csslint" selected>CSSLint</option>
|
|
||||||
<option value="stylelint">Stylelint</option>
|
|
||||||
<option value="" i18n="genericDisabledLabel"></option>
|
|
||||||
</select>
|
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
|
||||||
<a id="linter-settings" class="svg-inline-wrapper" i18n="title:linterConfigTooltip" tabindex="0">
|
|
||||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<span class="svg-inline-wrapper">
|
||||||
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
|
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
<summary><h2 i18n="publish"></h2></summary>
|
</span>
|
||||||
<div>
|
</div>
|
||||||
<a id="usw-url" href="https://userstyles.world" target="_blank"> </a>
|
<div class="option aligned">
|
||||||
<div id="usw-link-info">
|
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
||||||
<dl><dt i18n="styleName"></dt><dd data-usw="name"></dd></dl>
|
<div class="select-resizer">
|
||||||
<dl><dt i18n="genericDescription"></dt><dd data-usw="description"></dd></dl>
|
<select id="editor.theme"></select>
|
||||||
</div>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
<div>
|
|
||||||
<button id="usw-publish-style"
|
|
||||||
i18n="data-publish:publishStyle, data-push:publishPush"></button>
|
|
||||||
<button id="usw-disconnect" i18n="optionsSyncDisconnect"></button>
|
|
||||||
<span id="usw-progress"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</div>
|
||||||
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">
|
<div class="option aligned">
|
||||||
<summary><h2 i18n="sections"></h2></summary>
|
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
||||||
<ol id="toc"></ol>
|
<div class="select-resizer">
|
||||||
</details>
|
<select id="editor.matchHighlight">
|
||||||
<details id="lint" data-pref="editor.lint.expanded" class="ignore-pref-if-compact" hidden>
|
<option i18n-text="cm_matchHighlightToken" value="token">
|
||||||
<summary>
|
<option i18n-text="cm_matchHighlightSelection" value="selection">
|
||||||
<h2><span i18n="linterIssues"></span><span id="issue-count"></span>
|
<option i18n-text="genericDisabledLabel" value="">
|
||||||
<a id="lint-help" class="svg-inline-wrapper intercepts-click" tabindex="0">
|
</select>
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
</a>
|
</div>
|
||||||
</h2>
|
</div>
|
||||||
</summary>
|
<div class="option aligned">
|
||||||
<div class="lint-report-container"></div>
|
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
|
||||||
</details>
|
<div class="select-resizer">
|
||||||
</div>
|
<select id="editor.linter">
|
||||||
<div id="header-resizer" i18n="title:headerResizerHint"></div>
|
<option value="csslint" selected>CSSLint</option>
|
||||||
|
<option value="stylelint">Stylelint</option>
|
||||||
|
<option value="" i18n-text="genericDisabledLabel"></option>
|
||||||
|
</select>
|
||||||
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="svg-inline-wrapper" i18n-title="linterConfigTooltip">
|
||||||
|
<svg id="linter-settings" class="svg-icon settings">
|
||||||
|
<use xlink:href="#svg-icon-settings"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details id="lint" class="hidden" data-pref="editor.lint.expanded">
|
||||||
|
<summary>
|
||||||
|
<h2 i18n-text="linterIssues">: <span id="issue-count"></span><!-- EAT SPACE
|
||||||
|
--><svg id="lint-help" class="svg-icon info intercepts-click">
|
||||||
|
<use xlink:href="#svg-icon-help"/>
|
||||||
|
</svg>
|
||||||
|
</h2>
|
||||||
|
</summary>
|
||||||
|
<div></div>
|
||||||
|
</details>
|
||||||
<div id="footer" class="hidden">
|
<div id="footer" class="hidden">
|
||||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||||
i18n="externalUsercssDocument"
|
i18n-text="externalUsercssDocument"
|
||||||
target="_blank"></a>
|
target="_blank"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section id="sections"></section>
|
<section id="sections">
|
||||||
|
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||||
|
</section>
|
||||||
<div id="help-popup">
|
<div id="help-popup">
|
||||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||||
<div class="contents"></div>
|
<div class="contents"></div>
|
||||||
|
@ -460,22 +336,16 @@
|
||||||
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
|
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n="alt:helpAlt">
|
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n-alt="helpAlt">
|
||||||
<circle cx="7" cy="5" r="1"/>
|
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||||
<path d="M8,8c0-0.5-0.5-1-1-1H6C5.5,7,5,7.4,5,8h1v3c0,0.5,0.5,1,1,1h1c0.5,0,1-0.4,1-1H8V8z"/>
|
|
||||||
<path d="M7,1c3.9,0,7,3.1,7,7s-3.1,7-7,7s-7-3.1-7-7S3.1,1,7,1z M7,2.3C3.9,2.3,1.3,4.9,1.3,8s2.6,5.7,5.7,5.7s5.7-2.6,5.7-5.7S10.1,2.3,7,2.3C7,2.3,7,2.3,7,2.3z"/>
|
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-close" viewBox="0 0 12 16">
|
<symbol id="svg-icon-close" viewBox="0 0 12 16">
|
||||||
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-v" viewBox="0 0 16 16">
|
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
|
||||||
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
|
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"/>
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol id="svg-icon-config" viewBox="0 0 16 16">
|
|
||||||
<path d="M13.3,12.8l1.5-2.6l-2.2-1.5c0-0.2,0.1-0.5,0.1-0.7c0-0.2,0-0.5-0.1-0.7l2.2-1.5l-1.5-2.6l-2.4,1.2 c-0.4-0.3-0.8-0.5-1.2-0.7L9.5,1h-3L6.3,3.7C5.9,3.8,5.5,4.1,5.1,4.4L2.7,3.2L1.2,5.8l2.2,1.5c0,0.2-0.1,0.5-0.1,0.7 c0,0.2,0,0.5,0.1,0.7l-2.2,1.5l1.5,2.6l2.4-1.2c0.4,0.3,0.8,0.5,1.2,0.7L6.5,15h3l0.2-2.7c0.4-0.2,0.8-0.4,1.2-0.7L13.3,12.8z M8,10.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,1-2.3,2.3-2.3c1.3,0,2.3,1,2.3,2.3C10.3,9.3,9.3,10.3,8,10.3z"/>
|
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
||||||
|
@ -486,22 +356,7 @@
|
||||||
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
|
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-plus" viewBox="0 0 8 8">
|
|
||||||
<path d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol id="svg-icon-minus" viewBox="0 0 8 8">
|
|
||||||
<path d="M0 3v2h8v-2h-8z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
|
||||||
|
|
||||||
<link href="edit/edit.css" rel="stylesheet">
|
|
||||||
<script src="js/dark-themer.js"></script> <!-- must be last in HEAD to avoid FOUC -->
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body id="stylus-edit">
|
|
||||||
<script src="edit/edit.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
585
edit/applies-to-line-widget.js
Normal file
585
edit/applies-to-line-widget.js
Normal file
|
@ -0,0 +1,585 @@
|
||||||
|
/* global regExpTester debounce messageBox CodeMirror template */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createAppliesToLineWidget(cm) {
|
||||||
|
const THROTTLE_DELAY = 400;
|
||||||
|
let TPL, EVENTS, CLICK_ROUTE;
|
||||||
|
let widgets = [];
|
||||||
|
let fromLine, toLine, actualStyle;
|
||||||
|
let initialized = false;
|
||||||
|
return {toggle};
|
||||||
|
|
||||||
|
function toggle(newState = !initialized) {
|
||||||
|
newState = Boolean(newState);
|
||||||
|
if (newState !== initialized) {
|
||||||
|
if (newState) {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
uninit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
TPL = {
|
||||||
|
container:
|
||||||
|
$create('div.applies-to', [
|
||||||
|
$create('label', t('appliesLabel')),
|
||||||
|
$create('ul.applies-to-list'),
|
||||||
|
]),
|
||||||
|
listItem: template.appliesTo,
|
||||||
|
appliesToEverything:
|
||||||
|
$create('li.applies-to-everything', t('appliesToEverything')),
|
||||||
|
};
|
||||||
|
|
||||||
|
$('button', TPL.listItem).insertAdjacentElement('beforebegin',
|
||||||
|
$create('button.test-regexp', t('styleRegexpTestButton')));
|
||||||
|
|
||||||
|
CLICK_ROUTE = {
|
||||||
|
'.test-regexp': showRegExpTester,
|
||||||
|
|
||||||
|
'.remove-applies-to': (item, apply) => {
|
||||||
|
const applies = item.closest('.applies-to').__applies;
|
||||||
|
const i = applies.indexOf(apply);
|
||||||
|
let repl;
|
||||||
|
let from;
|
||||||
|
let to;
|
||||||
|
if (applies.length < 2) {
|
||||||
|
messageBox({
|
||||||
|
contents: t('appliesRemoveError'),
|
||||||
|
buttons: [t('confirmClose')]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (i === 0) {
|
||||||
|
from = apply.mark.find().from;
|
||||||
|
to = applies[i + 1].mark.find().from;
|
||||||
|
repl = '';
|
||||||
|
} else if (i === applies.length - 1) {
|
||||||
|
from = applies[i - 1].mark.find().to;
|
||||||
|
to = apply.mark.find().to;
|
||||||
|
repl = '';
|
||||||
|
} else {
|
||||||
|
from = applies[i - 1].mark.find().to;
|
||||||
|
to = applies[i + 1].mark.find().from;
|
||||||
|
repl = ', ';
|
||||||
|
}
|
||||||
|
cm.replaceRange(repl, from, to, 'appliesTo');
|
||||||
|
clearApply(apply);
|
||||||
|
item.remove();
|
||||||
|
applies.splice(i, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
'.add-applies-to': (item, apply) => {
|
||||||
|
const applies = this.closest('.applies-to').__applies;
|
||||||
|
const i = applies.indexOf(apply);
|
||||||
|
const pos = apply.mark.find().to;
|
||||||
|
const text = `, ${apply.type.text}("")`;
|
||||||
|
cm.replaceRange(text, pos, pos, 'appliesTo');
|
||||||
|
const newApply = createApply(
|
||||||
|
cm.indexFromPos(pos) + 2,
|
||||||
|
apply.type.text,
|
||||||
|
'',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
setupApplyMarkers(newApply);
|
||||||
|
applies.splice(i + 1, 0, newApply);
|
||||||
|
item.insertAdjacentElement('afterend', buildChildren(applies, newApply));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
EVENTS = {
|
||||||
|
onchange({target}) {
|
||||||
|
const typeElement = target.closest('.applies-type');
|
||||||
|
if (typeElement) {
|
||||||
|
const item = target.closest('.applies-to-item');
|
||||||
|
const apply = item.__apply;
|
||||||
|
changeItem(item, apply, 'type', typeElement.value);
|
||||||
|
item.dataset.type = apply.type.text;
|
||||||
|
} else {
|
||||||
|
return EVENTS.oninput.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oninput({target}) {
|
||||||
|
if (target.matches('.applies-value')) {
|
||||||
|
const item = target.closest('.applies-to-item');
|
||||||
|
const apply = item.__apply;
|
||||||
|
changeItem(item, apply, 'value', target.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclick({target}) {
|
||||||
|
for (const selector in CLICK_ROUTE) {
|
||||||
|
const routed = target.closest(selector);
|
||||||
|
if (routed) {
|
||||||
|
const item = routed.closest('.applies-to-item');
|
||||||
|
CLICK_ROUTE[selector].call(routed, item, item.__apply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actualStyle = $create('style');
|
||||||
|
fromLine = 0;
|
||||||
|
toLine = cm.doc.size;
|
||||||
|
|
||||||
|
cm.on('change', onChange);
|
||||||
|
cm.on('optionChange', onOptionChange);
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||||
|
|
||||||
|
updateWidgetStyle();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninit() {
|
||||||
|
initialized = false;
|
||||||
|
|
||||||
|
widgets.forEach(clearWidget);
|
||||||
|
widgets.length = 0;
|
||||||
|
cm.off('change', onChange);
|
||||||
|
cm.off('optionChange', onOptionChange);
|
||||||
|
chrome.runtime.onMessage.removeListener(onRuntimeMessage);
|
||||||
|
actualStyle.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(cm, event) {
|
||||||
|
const {from, to, origin} = event;
|
||||||
|
if (origin === 'appliesTo') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastChanged = CodeMirror.changeEnd(event).line;
|
||||||
|
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
|
||||||
|
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
|
||||||
|
if (origin === 'setValue') {
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
debounce(update, THROTTLE_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOptionChange(cm, option) {
|
||||||
|
if (option === 'theme') {
|
||||||
|
updateWidgetStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRuntimeMessage(msg) {
|
||||||
|
if (msg.style || msg.styles ||
|
||||||
|
msg.prefs && 'disableAll' in msg.prefs ||
|
||||||
|
msg.method === 'styleDeleted') {
|
||||||
|
requestAnimationFrame(updateWidgetStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const changed = {fromLine, toLine};
|
||||||
|
fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
|
||||||
|
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
|
||||||
|
const visible = {fromLine, toLine};
|
||||||
|
if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
|
||||||
|
cm.operation(doUpdate);
|
||||||
|
}
|
||||||
|
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
|
||||||
|
setTimeout(updateInvisible, 0, changed, visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInvisible(changed, visible) {
|
||||||
|
let inOp = false;
|
||||||
|
if (changed.fromLine < visible.fromLine) {
|
||||||
|
fromLine = Math.min(fromLine, changed.fromLine);
|
||||||
|
toLine = Math.min(changed.toLine, visible.fromLine);
|
||||||
|
inOp = true;
|
||||||
|
cm.startOperation();
|
||||||
|
doUpdate();
|
||||||
|
}
|
||||||
|
if (changed.toLine > visible.toLine) {
|
||||||
|
fromLine = Math.max(fromLine, changed.toLine);
|
||||||
|
toLine = Math.max(changed.toLine, visible.toLine);
|
||||||
|
if (!inOp) {
|
||||||
|
inOp = true;
|
||||||
|
cm.startOperation();
|
||||||
|
}
|
||||||
|
doUpdate();
|
||||||
|
}
|
||||||
|
if (inOp) {
|
||||||
|
cm.endOperation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidgetStyle() {
|
||||||
|
const MIN_LUMA = .05;
|
||||||
|
const MIN_LUMA_DIFF = .4;
|
||||||
|
const color = {
|
||||||
|
wrapper: getRealColors(cm.display.wrapper),
|
||||||
|
gutter: getRealColors(cm.display.gutters, {
|
||||||
|
bg: 'backgroundColor',
|
||||||
|
border: 'borderRightColor',
|
||||||
|
}),
|
||||||
|
line: getRealColors('.CodeMirror-linenumber'),
|
||||||
|
comment: getRealColors('span.cm-comment'),
|
||||||
|
};
|
||||||
|
const hasBorder =
|
||||||
|
color.gutter.style.borderRightWidth !== '0px' &&
|
||||||
|
!/transparent|\b0\)/g.test(color.gutter.style.borderRightColor);
|
||||||
|
const diff = {
|
||||||
|
wrapper: Math.abs(color.gutter.bgLuma - color.wrapper.foreLuma),
|
||||||
|
border: hasBorder ? Math.abs(color.gutter.bgLuma - color.gutter.borderLuma) : 0,
|
||||||
|
line: Math.abs(color.gutter.bgLuma - color.line.foreLuma),
|
||||||
|
};
|
||||||
|
const preferLine = diff.line > diff.wrapper || diff.line > MIN_LUMA_DIFF;
|
||||||
|
const fore = preferLine ? color.line.fore : color.wrapper.fore;
|
||||||
|
|
||||||
|
const border = fore.replace(/[\d.]+(?=\))/, MIN_LUMA_DIFF / 2);
|
||||||
|
const borderStyleForced = `1px ${hasBorder ? color.gutter.style.borderRightStyle : 'solid'} ${border}`;
|
||||||
|
|
||||||
|
actualStyle.textContent = `
|
||||||
|
.applies-to {
|
||||||
|
background-color: ${color.gutter.bg};
|
||||||
|
border-top: ${borderStyleForced};
|
||||||
|
border-bottom: ${borderStyleForced};
|
||||||
|
}
|
||||||
|
.applies-to label {
|
||||||
|
color: ${fore};
|
||||||
|
}
|
||||||
|
.applies-to input,
|
||||||
|
.applies-to select {
|
||||||
|
background-color: rgba(255, 255, 255, ${
|
||||||
|
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
|
||||||
|
});
|
||||||
|
border: ${borderStyleForced};
|
||||||
|
transition: none;
|
||||||
|
color: ${fore};
|
||||||
|
}
|
||||||
|
.applies-to .svg-icon.select-arrow {
|
||||||
|
fill: ${fore};
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.documentElement.appendChild(actualStyle);
|
||||||
|
|
||||||
|
function getRealColors(el, targets = {}) {
|
||||||
|
targets.fore = 'color';
|
||||||
|
const colors = {};
|
||||||
|
const done = {};
|
||||||
|
let numDone = 0;
|
||||||
|
let numTotal = 0;
|
||||||
|
for (const k in targets) {
|
||||||
|
colors[k] = {r: 255, g: 255, b: 255, a: 1};
|
||||||
|
numTotal++;
|
||||||
|
}
|
||||||
|
const isDummy = typeof el === 'string';
|
||||||
|
el = isDummy ? cm.display.lineDiv.appendChild($create(el, {style: 'display: none'})) : el;
|
||||||
|
for (let current = el; current; current = current && current.parentElement) {
|
||||||
|
const style = getComputedStyle(current);
|
||||||
|
for (const k in targets) {
|
||||||
|
if (!done[k]) {
|
||||||
|
done[k] = blend(colors[k], style[targets[k]]);
|
||||||
|
numDone += done[k] ? 1 : 0;
|
||||||
|
if (numDone === numTotal) {
|
||||||
|
current = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colors.style = colors.style || style;
|
||||||
|
}
|
||||||
|
if (isDummy) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
for (const k in targets) {
|
||||||
|
const {r, g, b, a} = colors[k];
|
||||||
|
colors[k] = `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
// https://www.w3.org/TR/AERT#color-contrast
|
||||||
|
colors[k + 'Luma'] = (r * .299 + g * .587 + b * .114) / 256;
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blend(base, color) {
|
||||||
|
const [r, g, b, a = 255] = (color.match(/\d+/g) || []).map(Number);
|
||||||
|
if (a === 255) {
|
||||||
|
base.r = r;
|
||||||
|
base.g = g;
|
||||||
|
base.b = b;
|
||||||
|
base.a = 1;
|
||||||
|
} else if (a) {
|
||||||
|
const mixedA = 1 - (1 - a / 255) * (1 - base.a);
|
||||||
|
const q1 = a / 255 / mixedA;
|
||||||
|
const q2 = base.a * (1 - mixedA) / mixedA;
|
||||||
|
base.r = Math.round(r * q1 + base.r * q2);
|
||||||
|
base.g = Math.round(g * q1 + base.g * q2);
|
||||||
|
base.b = Math.round(b * q1 + base.b * q2);
|
||||||
|
base.a = mixedA;
|
||||||
|
}
|
||||||
|
return Math.abs(base.a - 1) < 1e-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doUpdate() {
|
||||||
|
// find which widgets needs to be update
|
||||||
|
// some widgets (lines) might be deleted
|
||||||
|
widgets = widgets.filter(w => w.line.lineNo() !== null);
|
||||||
|
let i = widgets.findIndex(w => w.line.lineNo() > fromLine) - 1;
|
||||||
|
let j = widgets.findIndex(w => w.line.lineNo() > toLine);
|
||||||
|
if (i === -2) {
|
||||||
|
i = widgets.length - 1;
|
||||||
|
}
|
||||||
|
if (j < 0) {
|
||||||
|
j = widgets.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decide search range
|
||||||
|
const fromPos = {line: widgets[i] ? widgets[i].line.lineNo() : 0, ch: 0};
|
||||||
|
const toPos = {line: widgets[j] ? widgets[j].line.lineNo() : toLine + 1, ch: 0};
|
||||||
|
|
||||||
|
// calc index->pos lookup table
|
||||||
|
let line = 0;
|
||||||
|
let index = 0;
|
||||||
|
let fromIndex, toIndex;
|
||||||
|
const lineIndexes = [index];
|
||||||
|
cm.doc.iter(({text}) => {
|
||||||
|
fromIndex = line === fromPos.line ? index : fromIndex;
|
||||||
|
lineIndexes.push((index += text.length + 1));
|
||||||
|
line++;
|
||||||
|
toIndex = line >= toPos.line ? index : toIndex;
|
||||||
|
return toIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
// splice
|
||||||
|
i = Math.max(0, i);
|
||||||
|
widgets.splice(i, 0, ...createWidgets(fromIndex, toIndex, widgets.splice(i, j - i), lineIndexes));
|
||||||
|
|
||||||
|
fromLine = null;
|
||||||
|
toLine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function *createWidgets(start, end, removed, lineIndexes) {
|
||||||
|
let i = 0;
|
||||||
|
let itemHeight;
|
||||||
|
for (const section of findAppliesTo(start, end)) {
|
||||||
|
let removedWidget = removed[i];
|
||||||
|
while (removedWidget && removedWidget.line.lineNo() < section.pos.line) {
|
||||||
|
clearWidget(removed[i]);
|
||||||
|
removedWidget = removed[++i];
|
||||||
|
}
|
||||||
|
for (const a of section.applies) {
|
||||||
|
setupApplyMarkers(a, lineIndexes);
|
||||||
|
}
|
||||||
|
if (removedWidget && removedWidget.line.lineNo() === section.pos.line) {
|
||||||
|
// reuse old widget
|
||||||
|
removedWidget.section.applies.forEach(apply => {
|
||||||
|
apply.type.mark.clear();
|
||||||
|
apply.value.mark.clear();
|
||||||
|
});
|
||||||
|
removedWidget.section = section;
|
||||||
|
const newNode = buildElement(section);
|
||||||
|
const removedNode = removedWidget.node;
|
||||||
|
if (removedNode.parentNode) {
|
||||||
|
removedNode.parentNode.replaceChild(newNode, removedNode);
|
||||||
|
}
|
||||||
|
removedWidget.node = newNode;
|
||||||
|
removedWidget.changed();
|
||||||
|
yield removedWidget;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// new widget
|
||||||
|
const widget = cm.addLineWidget(section.pos.line, buildElement(section), {
|
||||||
|
coverGutter: true,
|
||||||
|
noHScroll: true,
|
||||||
|
above: true,
|
||||||
|
height: itemHeight ? section.applies.length * itemHeight : undefined,
|
||||||
|
});
|
||||||
|
widget.section = section;
|
||||||
|
itemHeight = itemHeight || widget.node.offsetHeight / (section.applies.length || 1);
|
||||||
|
yield widget;
|
||||||
|
}
|
||||||
|
removed.slice(i).forEach(clearWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWidget(widget) {
|
||||||
|
widget.clear();
|
||||||
|
widget.section.applies.forEach(clearApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearApply(apply) {
|
||||||
|
apply.type.mark.clear();
|
||||||
|
apply.value.mark.clear();
|
||||||
|
apply.mark.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupApplyMarkers(apply, lineIndexes) {
|
||||||
|
apply.type.mark = cm.markText(
|
||||||
|
posFromIndex(cm, apply.type.start, lineIndexes),
|
||||||
|
posFromIndex(cm, apply.type.end, lineIndexes),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
apply.value.mark = cm.markText(
|
||||||
|
posFromIndex(cm, apply.value.start, lineIndexes),
|
||||||
|
posFromIndex(cm, apply.value.end, lineIndexes),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
apply.mark = cm.markText(
|
||||||
|
posFromIndex(cm, apply.start, lineIndexes),
|
||||||
|
posFromIndex(cm, apply.end, lineIndexes),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function posFromIndex(cm, index, lineIndexes) {
|
||||||
|
if (!lineIndexes) {
|
||||||
|
return cm.posFromIndex(index);
|
||||||
|
}
|
||||||
|
let line = lineIndexes.prev || 0;
|
||||||
|
const prev = lineIndexes[line];
|
||||||
|
const next = lineIndexes[line + 1];
|
||||||
|
if (prev <= index && index < next) {
|
||||||
|
return {line, ch: index - prev};
|
||||||
|
}
|
||||||
|
let a = index < prev ? 0 : line;
|
||||||
|
let b = index < next ? line + 1 : lineIndexes.length - 1;
|
||||||
|
while (a < b - 1) {
|
||||||
|
const mid = (a + b) >> 1;
|
||||||
|
if (lineIndexes[mid] < index) {
|
||||||
|
a = mid;
|
||||||
|
} else {
|
||||||
|
b = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line = lineIndexes[b] > index ? a : b;
|
||||||
|
Object.defineProperty(lineIndexes, 'prev', {value: line, configurable: true});
|
||||||
|
return {line, ch: index - lineIndexes[line]};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildElement({applies}) {
|
||||||
|
const container = TPL.container.cloneNode(true);
|
||||||
|
const list = $('.applies-to-list', container);
|
||||||
|
for (const apply of applies) {
|
||||||
|
list.appendChild(buildChildren(applies, apply));
|
||||||
|
}
|
||||||
|
if (!list.children[0]) {
|
||||||
|
list.appendChild(TPL.appliesToEverything.cloneNode(true));
|
||||||
|
}
|
||||||
|
return Object.assign(container, EVENTS, {__applies: applies});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChildren(applies, apply) {
|
||||||
|
const el = TPL.listItem.cloneNode(true);
|
||||||
|
el.dataset.type = apply.type.text;
|
||||||
|
el.__apply = apply;
|
||||||
|
$('.applies-type', el).value = apply.type.text;
|
||||||
|
$('.applies-value', el).value = apply.value.text;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeItem(itemElement, apply, part, newText) {
|
||||||
|
if (!apply) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
part = apply[part];
|
||||||
|
const range = part.mark.find();
|
||||||
|
part.mark.clear();
|
||||||
|
newText = unescapeDoubleslash(newText).replace(/\\/g, '\\\\');
|
||||||
|
cm.replaceRange(newText, range.from, range.to, 'appliesTo');
|
||||||
|
part.mark = cm.markText(
|
||||||
|
range.from,
|
||||||
|
cm.findPosH(range.from, newText.length, 'char'),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
part.text = newText;
|
||||||
|
|
||||||
|
if (part === apply.type) {
|
||||||
|
const range = apply.mark.find();
|
||||||
|
apply.mark.clear();
|
||||||
|
apply.mark = cm.markText(
|
||||||
|
part.mark.find().from,
|
||||||
|
range.to,
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apply.type.text === 'regexp' && apply.value.text.trim()) {
|
||||||
|
showRegExpTester(itemElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApply(pos, typeText, valueText, isQuoted = false) {
|
||||||
|
typeText = typeText.toLowerCase();
|
||||||
|
const start = pos;
|
||||||
|
const typeStart = start;
|
||||||
|
const typeEnd = typeStart + typeText.length;
|
||||||
|
const valueStart = typeEnd + 1 + Number(isQuoted);
|
||||||
|
const valueEnd = valueStart + valueText.length;
|
||||||
|
const end = valueEnd + Number(isQuoted) + 1;
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
type: {
|
||||||
|
text: typeText,
|
||||||
|
start: typeStart,
|
||||||
|
end: typeEnd,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
text: unescapeDoubleslash(valueText),
|
||||||
|
start: valueStart,
|
||||||
|
end: valueEnd,
|
||||||
|
},
|
||||||
|
end
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function *findAppliesTo(posStart, posEnd) {
|
||||||
|
const text = cm.getValue();
|
||||||
|
const re = /^[\t ]*@-moz-document[\s\n]+/gm;
|
||||||
|
const applyRe = new RegExp([
|
||||||
|
/(?:\/\*[^*]*\*\/[\s\n]*)*/,
|
||||||
|
/(url|url-prefix|domain|regexp)/,
|
||||||
|
/\(((['"])(?:\\\\|\\\n|\\\3|[^\n])*?\3|[^)\n]*)\)\s*(,\s*)?/,
|
||||||
|
].map(rx => rx.source).join(''), 'giy');
|
||||||
|
let match;
|
||||||
|
re.lastIndex = posStart;
|
||||||
|
while ((match = re.exec(text))) {
|
||||||
|
if (match.index >= posEnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const applies = [];
|
||||||
|
let m;
|
||||||
|
applyRe.lastIndex = re.lastIndex;
|
||||||
|
while ((m = applyRe.exec(text))) {
|
||||||
|
const apply = createApply(
|
||||||
|
m.index,
|
||||||
|
m[1],
|
||||||
|
unquote(m[2]),
|
||||||
|
unquote(m[2]) !== m[2]
|
||||||
|
);
|
||||||
|
applies.push(apply);
|
||||||
|
re.lastIndex = applyRe.lastIndex;
|
||||||
|
}
|
||||||
|
yield {
|
||||||
|
pos: cm.posFromIndex(match.index),
|
||||||
|
applies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unquote(s) {
|
||||||
|
const first = s.charAt(0);
|
||||||
|
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unescapeDoubleslash(s) {
|
||||||
|
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/.test(s);
|
||||||
|
return hasSingleEscapes ? s : s.replace(/\\\\/g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRegExpTester(item) {
|
||||||
|
regExpTester.toggle(true);
|
||||||
|
regExpTester.update(
|
||||||
|
item.closest('.applies-to').__applies
|
||||||
|
.filter(a => a.type.text === 'regexp')
|
||||||
|
.map(a => unescapeDoubleslash(a.value.text)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,274 +0,0 @@
|
||||||
/* global CodeMirror */
|
|
||||||
/* global cmFactory */
|
|
||||||
/* global debounce */// toolbox.js
|
|
||||||
/* global editor */
|
|
||||||
/* global linterMan */
|
|
||||||
/* global prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/* Registers 'hint' helper and 'autocompleteOnTyping' option in CodeMirror */
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const USO_VAR = 'uso-variable';
|
|
||||||
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
|
|
||||||
const USO_INVALID_VAR = 'error ' + USO_VAR;
|
|
||||||
const rxPROP = /^(prop(erty)?|variable-2|string-2)\b/;
|
|
||||||
const rxVAR = /(^|[^-.\w\u0080-\uFFFF])var\(/iyu;
|
|
||||||
const rxCONSUME = /([-\w]*\s*:\s?)?/yu;
|
|
||||||
const cssMime = CodeMirror.mimeModes['text/css'];
|
|
||||||
const docFuncs = addSuffix(cssMime.documentTypes, '(');
|
|
||||||
const {tokenHooks} = cssMime;
|
|
||||||
const originalCommentHook = tokenHooks['/'];
|
|
||||||
const originalHelper = CodeMirror.hint.css || (() => {});
|
|
||||||
let cssMedia, cssProps, cssValues;
|
|
||||||
|
|
||||||
const AOT_ID = 'autocompleteOnTyping';
|
|
||||||
const AOT_PREF_ID = 'editor.' + AOT_ID;
|
|
||||||
const aot = prefs.get(AOT_PREF_ID);
|
|
||||||
CodeMirror.defineOption(AOT_ID, aot, (cm, value) => {
|
|
||||||
cm[value ? 'on' : 'off']('changes', autocompleteOnTyping);
|
|
||||||
cm[value ? 'on' : 'off']('pick', autocompletePicked);
|
|
||||||
});
|
|
||||||
prefs.subscribe(AOT_PREF_ID, (key, val) => cmFactory.globalSetOption(AOT_ID, val), {runNow: aot});
|
|
||||||
|
|
||||||
CodeMirror.registerHelper('hint', 'css', helper);
|
|
||||||
CodeMirror.registerHelper('hint', 'stylus', helper);
|
|
||||||
|
|
||||||
tokenHooks['/'] = tokenizeUsoVariables;
|
|
||||||
|
|
||||||
async function helper(cm) {
|
|
||||||
const pos = cm.getCursor();
|
|
||||||
const {line, ch} = pos;
|
|
||||||
const {styles, text} = cm.getLineHandle(line);
|
|
||||||
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
|
|
||||||
const isLessLang = cm.doc.mode.helperType === 'less';
|
|
||||||
const isStylusLang = cm.doc.mode.name === 'stylus';
|
|
||||||
const type = style && style.split(' ', 1)[0] || 'prop?';
|
|
||||||
if (!type || type === 'comment' || type === 'string') {
|
|
||||||
return originalHelper(cm);
|
|
||||||
}
|
|
||||||
// not using getTokenAt until the need is unavoidable because it reparses text
|
|
||||||
// and runs a whole lot of complex calc inside which is slow on long lines
|
|
||||||
// especially if autocomplete is auto-shown on each keystroke
|
|
||||||
let prev, end, state;
|
|
||||||
let i = index;
|
|
||||||
while (
|
|
||||||
(prev == null || `${styles[i - 1]}`.startsWith(type)) &&
|
|
||||||
(prev = i > 2 ? styles[i - 2] : 0) &&
|
|
||||||
isSameToken(text, style, prev)
|
|
||||||
) i -= 2;
|
|
||||||
i = index;
|
|
||||||
while (
|
|
||||||
(end == null || `${styles[i + 1]}`.startsWith(type)) &&
|
|
||||||
(end = styles[i]) &&
|
|
||||||
isSameToken(text, style, end)
|
|
||||||
) i += 2;
|
|
||||||
const getTokenState = () => state || (state = cm.getTokenAt(pos, true).state.state);
|
|
||||||
const str = text.slice(prev, end);
|
|
||||||
const left = text.slice(prev, ch).trim();
|
|
||||||
let leftLC = left.toLowerCase();
|
|
||||||
let list;
|
|
||||||
switch (leftLC[0]) {
|
|
||||||
|
|
||||||
case '!':
|
|
||||||
list = '!important'.startsWith(leftLC) ? ['!important'] : [];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '@':
|
|
||||||
list = [
|
|
||||||
'@-moz-document',
|
|
||||||
'@charset',
|
|
||||||
'@font-face',
|
|
||||||
'@import',
|
|
||||||
'@keyframes',
|
|
||||||
'@media',
|
|
||||||
'@namespace',
|
|
||||||
'@page',
|
|
||||||
'@supports',
|
|
||||||
'@viewport',
|
|
||||||
];
|
|
||||||
if (isLessLang) list = findAllCssVars(cm, left, '\\s*:').concat(list);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '#': // prevents autocomplete for #hex colors
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '-': // --variable
|
|
||||||
case '(': // var(
|
|
||||||
list = str.startsWith('--') || testAt(rxVAR, ch - 5, text)
|
|
||||||
? findAllCssVars(cm, left)
|
|
||||||
: [];
|
|
||||||
if (str.startsWith('(')) {
|
|
||||||
prev++;
|
|
||||||
leftLC = left.slice(1);
|
|
||||||
} else {
|
|
||||||
leftLC = left;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '/': // USO vars
|
|
||||||
if (str.startsWith('/*[[') && str.endsWith(']]*/')) {
|
|
||||||
prev += 4;
|
|
||||||
end -= 4;
|
|
||||||
end -= text.slice(end - 4, end) === '-rgb' ? 4 : 0;
|
|
||||||
list = Object.keys((editor.style.usercssData || {}).vars || {}).sort();
|
|
||||||
leftLC = left.slice(4);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'u': // url(), url-prefix()
|
|
||||||
case 'd': // domain()
|
|
||||||
case 'r': // regexp()
|
|
||||||
if (/^(variable|tag|error)/.test(type) &&
|
|
||||||
docFuncs.some(s => s.startsWith(leftLC)) &&
|
|
||||||
/^(top|documentTypes|atBlock)/.test(getTokenState())) {
|
|
||||||
end++;
|
|
||||||
list = docFuncs;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// fallthrough to `default`
|
|
||||||
|
|
||||||
default:
|
|
||||||
// property values
|
|
||||||
if (isStylusLang || getTokenState() === 'prop') {
|
|
||||||
while (i > 0 && !rxPROP.test(styles[i + 1])) i -= 2;
|
|
||||||
const propEnd = styles[i];
|
|
||||||
let prop;
|
|
||||||
if (propEnd > text.lastIndexOf(';', ch - 1)) {
|
|
||||||
while (i > 0 && rxPROP.test(styles[i + 1])) i -= 2;
|
|
||||||
prop = text.slice(styles[i] || 0, propEnd).match(/([-\w]+)?$/u)[1];
|
|
||||||
}
|
|
||||||
if (prop) {
|
|
||||||
if (/[^-\w]/.test(leftLC)) {
|
|
||||||
prev += execAt(/[\s:()]*/y, prev, text)[0].length;
|
|
||||||
leftLC = leftLC.replace(/^[^\w\s]\s*/, '');
|
|
||||||
}
|
|
||||||
if (prop.startsWith('--')) prop = 'color'; // assuming 90% of variables are colors
|
|
||||||
if (!cssProps) await initCssProps();
|
|
||||||
list = [...new Set([...cssValues.all[prop] || [], ...cssValues.global])];
|
|
||||||
end = prev + execAt(/(\s*[-a-z(]+)?/y, prev, text)[0].length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// properties and media features
|
|
||||||
if (!list &&
|
|
||||||
/^(prop(erty|\?)|atom|error|tag)/.test(type) &&
|
|
||||||
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
|
|
||||||
if (!cssProps) await initCssProps();
|
|
||||||
if (type === 'prop?') {
|
|
||||||
prev += leftLC.length;
|
|
||||||
leftLC = '';
|
|
||||||
}
|
|
||||||
list = state === 'atBlock_parens' ? cssMedia : cssProps;
|
|
||||||
end -= /\W$/u.test(str); // e.g. don't consume ) when inside ()
|
|
||||||
end += execAt(rxCONSUME, end, text)[0].length;
|
|
||||||
|
|
||||||
}
|
|
||||||
if (!list) {
|
|
||||||
return isStylusLang
|
|
||||||
? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus})
|
|
||||||
: originalHelper(cm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
list: (list || []).filter(s => s.startsWith(leftLC)),
|
|
||||||
from: {line, ch: prev + str.match(/^\s*/)[0].length},
|
|
||||||
to: {line, ch: end},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initCssProps() {
|
|
||||||
cssValues = await linterMan.worker.getCssPropsValues();
|
|
||||||
cssProps = addSuffix(cssValues.all);
|
|
||||||
cssMedia = [].concat(...Object.entries(cssMime).map(getMediaKeys).filter(Boolean)).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSuffix(obj, suffix = ': ') {
|
|
||||||
// Sorting first, otherwise "foo-bar:" would precede "foo:"
|
|
||||||
return Object.keys(obj).sort().map(k => k + suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMediaKeys([k, v]) {
|
|
||||||
return k === 'mediaFeatures' && addSuffix(v) ||
|
|
||||||
k.startsWith('media') && Object.keys(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** makes sure we don't process a different adjacent comment */
|
|
||||||
function isSameToken(text, style, i) {
|
|
||||||
return !style || text[i] !== '/' && text[i + 1] !== '*' ||
|
|
||||||
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAllCssVars(cm, leftPart, rightPart = '') {
|
|
||||||
// simplified regex without CSS escapes
|
|
||||||
const [, prefixed, named] = leftPart.match(/^(--|@)?(\S)?/);
|
|
||||||
const rx = new RegExp(
|
|
||||||
'(?:^|[\\s/;{])(' +
|
|
||||||
(prefixed ? leftPart : '--') +
|
|
||||||
(named ? '' : '[a-zA-Z_\u0080-\uFFFF]') +
|
|
||||||
'[-0-9a-zA-Z_\u0080-\uFFFF]*)' +
|
|
||||||
rightPart,
|
|
||||||
'g');
|
|
||||||
const list = new Set();
|
|
||||||
cm.eachLine(({text}) => {
|
|
||||||
for (let m; (m = rx.exec(text));) {
|
|
||||||
list.add(m[1]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [...list].sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeUsoVariables(stream) {
|
|
||||||
const token = originalCommentHook.apply(this, arguments);
|
|
||||||
if (token[1] === 'comment') {
|
|
||||||
const {string, start, pos} = stream;
|
|
||||||
if (testAt(/\/\*\[\[/y, start, string) &&
|
|
||||||
testAt(/]]\*\//y, pos - 4, string)) {
|
|
||||||
const vars = (editor.style.usercssData || {}).vars;
|
|
||||||
token[0] =
|
|
||||||
vars && vars.hasOwnProperty(string.slice(start + 4, pos - 4).replace(/-rgb$/, ''))
|
|
||||||
? USO_VALID_VAR
|
|
||||||
: USO_INVALID_VAR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
function execAt(rx, index, text) {
|
|
||||||
rx.lastIndex = index;
|
|
||||||
return rx.exec(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function testAt(rx, index, text) {
|
|
||||||
rx.lastIndex = Math.max(0, index);
|
|
||||||
return rx.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function autocompleteOnTyping(cm, [info], debounced) {
|
|
||||||
const lastLine = info.text[info.text.length - 1];
|
|
||||||
if (cm.state.completionActive ||
|
|
||||||
info.origin && !info.origin.includes('input') ||
|
|
||||||
!lastLine) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cm.state.autocompletePicked) {
|
|
||||||
cm.state.autocompletePicked = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!debounced) {
|
|
||||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lastLine.match(/[-a-z!]+$/i)) {
|
|
||||||
cm.state.autocompletePicked = false;
|
|
||||||
cm.options.hintOptions.completeSingle = false;
|
|
||||||
cm.execCommand('autocomplete');
|
|
||||||
setTimeout(() => {
|
|
||||||
cm.options.hintOptions.completeSingle = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autocompletePicked(cm) {
|
|
||||||
cm.state.autocompletePicked = true;
|
|
||||||
}
|
|
||||||
})();
|
|
427
edit/base.js
427
edit/base.js
|
@ -1,427 +0,0 @@
|
||||||
/* global $$ $ $create messageBoxProxy setInputValue setupLivePrefs */// dom.js
|
|
||||||
/* global API */// msg.js
|
|
||||||
/* global CODEMIRROR_THEMES */
|
|
||||||
/* global CodeMirror */
|
|
||||||
/* global MozDocMapper */// sections-util.js
|
|
||||||
/* global chromeSync */// storage-util.js
|
|
||||||
/* global initBeautifyButton */// beautify.js
|
|
||||||
/* global prefs */
|
|
||||||
/* global t */// localization.js
|
|
||||||
/* global FIREFOX getOwnTab sessionStore tryJSONparse tryURL */// toolbox.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type Editor
|
|
||||||
* @namespace Editor
|
|
||||||
*/
|
|
||||||
const editor = {
|
|
||||||
style: null,
|
|
||||||
dirty: DirtyReporter(),
|
|
||||||
isUsercss: false,
|
|
||||||
isWindowed: false,
|
|
||||||
livePreview: LivePreview(),
|
|
||||||
/** @type {'customName'|'name'} */
|
|
||||||
nameTarget: 'name',
|
|
||||||
previewDelay: 200, // Chrome devtools uses 200
|
|
||||||
saving: false,
|
|
||||||
scrollInfo: null,
|
|
||||||
|
|
||||||
cancel: () => location.assign('/manage.html'),
|
|
||||||
|
|
||||||
updateClass() {
|
|
||||||
$.rootCL.toggle('is-new-style', !editor.style.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTheme(name) {
|
|
||||||
if (!CODEMIRROR_THEMES[name]) {
|
|
||||||
name = 'default';
|
|
||||||
prefs.set('editor.theme', name);
|
|
||||||
}
|
|
||||||
$('#cm-theme').dataset.theme = name;
|
|
||||||
$('#cm-theme').textContent = CODEMIRROR_THEMES[name] || '';
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTitle(isDirty = editor.dirty.isDirty()) {
|
|
||||||
const {customName, name} = editor.style;
|
|
||||||
document.title = `${
|
|
||||||
isDirty ? '* ' : ''
|
|
||||||
}${
|
|
||||||
customName || name || t('styleMissingName')
|
|
||||||
} - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//#region pre-init
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const mqCompact = matchMedia('(max-width: 850px)');
|
|
||||||
const toggleCompact = mq => $.rootCL.toggle('compact-layout', mq.matches);
|
|
||||||
mqCompact.on('change', toggleCompact);
|
|
||||||
toggleCompact(mqCompact);
|
|
||||||
Object.assign(editor, /** @namespace Editor */ {
|
|
||||||
mqCompact,
|
|
||||||
styleReady: prefs.ready.then(loadStyle),
|
|
||||||
});
|
|
||||||
async function loadStyle() {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
let id = Number(params.get('id'));
|
|
||||||
const style = id && await API.styles.get(id) || {
|
|
||||||
id: id = null, // resetting the non-existent id
|
|
||||||
name: params.get('domain') ||
|
|
||||||
tryURL(params.get('url-prefix')).hostname ||
|
|
||||||
'',
|
|
||||||
enabled: true,
|
|
||||||
sections: [
|
|
||||||
MozDocMapper.toSection([...params], {code: ''}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
|
|
||||||
const isUC = Boolean(style.usercssData || !id && prefs.get('newStyleAsUsercss'));
|
|
||||||
Object.assign(editor, /** @namespace Editor */ {
|
|
||||||
style,
|
|
||||||
isUsercss: isUC,
|
|
||||||
template: isUC && !id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate), // promise
|
|
||||||
});
|
|
||||||
editor.updateClass();
|
|
||||||
editor.updateTheme(prefs.get('editor.theme'));
|
|
||||||
editor.updateTitle(false);
|
|
||||||
$.rootCL.add(isUC ? 'usercss' : 'sectioned');
|
|
||||||
sessionStore.justEditedStyleId = id || '';
|
|
||||||
// no such style so let's clear the invalid URL parameters
|
|
||||||
if (!id) history.replaceState({}, '', location.pathname);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region init header
|
|
||||||
|
|
||||||
/* exported EditorHeader */
|
|
||||||
function EditorHeader() {
|
|
||||||
initBeautifyButton($('#beautify'));
|
|
||||||
initKeymapElement();
|
|
||||||
initNameArea();
|
|
||||||
initThemeElement();
|
|
||||||
setupLivePrefs();
|
|
||||||
|
|
||||||
window.on('load', () => {
|
|
||||||
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
|
|
||||||
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
function findKeyForCommand(command, map) {
|
|
||||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
|
||||||
let key = Object.keys(map).find(k => map[k] === command);
|
|
||||||
if (key) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
|
||||||
key = ft && findKeyForCommand(command, ft);
|
|
||||||
if (key) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function initNameArea() {
|
|
||||||
const nameEl = $('#name');
|
|
||||||
const resetEl = $('#reset-name');
|
|
||||||
const isCustomName = editor.style.updateUrl || editor.isUsercss;
|
|
||||||
editor.nameTarget = isCustomName ? 'customName' : 'name';
|
|
||||||
nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
|
||||||
nameEl.title = isCustomName ? t('customNameHint') : '';
|
|
||||||
nameEl.on('input', () => {
|
|
||||||
editor.updateName(true);
|
|
||||||
resetEl.hidden = !editor.style.customName;
|
|
||||||
});
|
|
||||||
resetEl.hidden = !editor.style.customName;
|
|
||||||
resetEl.onclick = () => {
|
|
||||||
editor.style.customName = null; // to delete it from db
|
|
||||||
setInputValue(nameEl, editor.style.name);
|
|
||||||
resetEl.hidden = true;
|
|
||||||
};
|
|
||||||
const enabledEl = $('#enabled');
|
|
||||||
enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initThemeElement() {
|
|
||||||
$('#editor.theme').append(...[
|
|
||||||
$create('option', {value: 'default'}, t('defaultTheme')),
|
|
||||||
...Object.keys(CODEMIRROR_THEMES).map(s => $create('option', s)),
|
|
||||||
]);
|
|
||||||
// move the theme after built-in CSS so that its same-specificity selectors win
|
|
||||||
document.head.appendChild($('#cm-theme'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initKeymapElement() {
|
|
||||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
|
||||||
const maps = Object.keys(CodeMirror.keyMap)
|
|
||||||
.map(name => ({
|
|
||||||
value: name,
|
|
||||||
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
|
||||||
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
let bin = fragment;
|
|
||||||
let groupName;
|
|
||||||
// group suffixed maps in <optgroup>
|
|
||||||
maps.forEach(({value, name}, i) => {
|
|
||||||
groupName = !name.includes('-') ? name : groupName;
|
|
||||||
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
|
||||||
if (groupWithNext) {
|
|
||||||
if (bin === fragment) {
|
|
||||||
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const el = bin.appendChild($create('option', {value}, name));
|
|
||||||
if (value === prefs.defaults['editor.keyMap']) {
|
|
||||||
el.dataset.default = '';
|
|
||||||
el.title = t('defaultTheme');
|
|
||||||
}
|
|
||||||
if (!groupWithNext) bin = fragment;
|
|
||||||
});
|
|
||||||
const selector = $('#editor.keyMap');
|
|
||||||
selector.textContent = '';
|
|
||||||
selector.appendChild(fragment);
|
|
||||||
selector.value = prefs.get('editor.keyMap');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
|
||||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
|
||||||
for (const el of $$('[data-hotkey-tooltip]')) {
|
|
||||||
if (el._hotkeyTooltipKeyMap !== mapName) {
|
|
||||||
el._hotkeyTooltipKeyMap = mapName;
|
|
||||||
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
|
||||||
const cmd = el.dataset.hotkeyTooltip;
|
|
||||||
const key = cmd[0] === '=' ? cmd.slice(1) :
|
|
||||||
findKeyForCommand(cmd, mapName) ||
|
|
||||||
extraKeys && findKeyForCommand(cmd, extraKeys);
|
|
||||||
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
|
||||||
if (el.title !== newTitle) el.title = newTitle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region init windowed mode
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
let ownTabId;
|
|
||||||
if (chrome.windows) {
|
|
||||||
initWindowedMode();
|
|
||||||
const pos = tryJSONparse(sessionStore.windowPos);
|
|
||||||
delete sessionStore.windowPos;
|
|
||||||
// resize the window on 'undo close'
|
|
||||||
if (pos && pos.left != null) {
|
|
||||||
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getOwnTab().then(tab => {
|
|
||||||
ownTabId = tab.id;
|
|
||||||
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
|
|
||||||
editor.cancel = () => history.back();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initWindowedMode() {
|
|
||||||
chrome.tabs.onAttached.addListener(onTabAttached);
|
|
||||||
// Chrome 96+ bug: the type is 'app' for a window that was restored via Ctrl-Shift-T
|
|
||||||
const isSimple = ['app', 'popup'].includes((await browser.windows.getCurrent()).type);
|
|
||||||
if (isSimple) require(['/edit/embedded-popup']);
|
|
||||||
editor.isWindowed = isSimple || (
|
|
||||||
history.length === 1 &&
|
|
||||||
await prefs.ready && prefs.get('openEditInWindow') &&
|
|
||||||
(await browser.windows.getAll()).length > 1 &&
|
|
||||||
(await browser.tabs.query({currentWindow: true})).length === 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onTabAttached(tabId, info) {
|
|
||||||
if (tabId !== ownTabId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (info.newPosition !== 0) {
|
|
||||||
prefs.set('openEditInWindow', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const win = await browser.windows.get(info.newWindowId, {populate: true});
|
|
||||||
// If there's only one tab in this window, it's been dragged to new window
|
|
||||||
const openEditInWindow = win.tabs.length === 1;
|
|
||||||
// FF-only because Chrome retardedly resets the size during dragging
|
|
||||||
if (openEditInWindow && FIREFOX) {
|
|
||||||
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
|
||||||
}
|
|
||||||
prefs.set('openEditInWindow', openEditInWindow);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region internals
|
|
||||||
|
|
||||||
/** @returns DirtyReporter */
|
|
||||||
function DirtyReporter() {
|
|
||||||
const data = new Map();
|
|
||||||
const listeners = new Set();
|
|
||||||
const dataListeners = new Set();
|
|
||||||
const notifyChange = wasDirty => {
|
|
||||||
const isDirty = data.size > 0;
|
|
||||||
const flipped = isDirty !== wasDirty;
|
|
||||||
if (flipped) {
|
|
||||||
listeners.forEach(cb => cb(isDirty));
|
|
||||||
}
|
|
||||||
if (flipped || isDirty) {
|
|
||||||
dataListeners.forEach(cb => cb(isDirty));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/** @namespace DirtyReporter */
|
|
||||||
return {
|
|
||||||
add(obj, value) {
|
|
||||||
const wasDirty = data.size > 0;
|
|
||||||
const saved = data.get(obj);
|
|
||||||
if (!saved) {
|
|
||||||
data.set(obj, {type: 'add', newValue: value});
|
|
||||||
} else if (saved.type === 'remove') {
|
|
||||||
if (saved.savedValue === value) {
|
|
||||||
data.delete(obj);
|
|
||||||
} else {
|
|
||||||
saved.newValue = value;
|
|
||||||
saved.type = 'modify';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifyChange(wasDirty);
|
|
||||||
},
|
|
||||||
clear(...objs) {
|
|
||||||
if (data.size && (
|
|
||||||
objs.length
|
|
||||||
? objs.map(data.delete, data).includes(true)
|
|
||||||
: (data.clear(), true)
|
|
||||||
)) {
|
|
||||||
notifyChange(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
has(key) {
|
|
||||||
return data.has(key);
|
|
||||||
},
|
|
||||||
isDirty() {
|
|
||||||
return data.size > 0;
|
|
||||||
},
|
|
||||||
modify(obj, oldValue, newValue) {
|
|
||||||
const wasDirty = data.size > 0;
|
|
||||||
const saved = data.get(obj);
|
|
||||||
if (!saved) {
|
|
||||||
if (oldValue !== newValue) {
|
|
||||||
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (saved.type === 'modify') {
|
|
||||||
if (saved.savedValue === newValue) {
|
|
||||||
data.delete(obj);
|
|
||||||
} else {
|
|
||||||
saved.newValue = newValue;
|
|
||||||
}
|
|
||||||
} else if (saved.type === 'add') {
|
|
||||||
saved.newValue = newValue;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifyChange(wasDirty);
|
|
||||||
},
|
|
||||||
onChange(cb, add = true) {
|
|
||||||
listeners[add ? 'add' : 'delete'](cb);
|
|
||||||
},
|
|
||||||
onDataChange(cb, add = true) {
|
|
||||||
dataListeners[add ? 'add' : 'delete'](cb);
|
|
||||||
},
|
|
||||||
remove(obj, value) {
|
|
||||||
const wasDirty = data.size > 0;
|
|
||||||
const saved = data.get(obj);
|
|
||||||
if (!saved) {
|
|
||||||
data.set(obj, {type: 'remove', savedValue: value});
|
|
||||||
} else if (saved.type === 'add') {
|
|
||||||
data.delete(obj);
|
|
||||||
} else if (saved.type === 'modify') {
|
|
||||||
saved.type = 'remove';
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifyChange(wasDirty);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function LivePreview() {
|
|
||||||
let el;
|
|
||||||
let data;
|
|
||||||
let port;
|
|
||||||
let preprocess;
|
|
||||||
let enabled = prefs.get('editor.livePreview');
|
|
||||||
|
|
||||||
prefs.subscribe('editor.livePreview', (key, value) => {
|
|
||||||
if (!value) {
|
|
||||||
if (port) {
|
|
||||||
port.disconnect();
|
|
||||||
port = null;
|
|
||||||
}
|
|
||||||
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
|
|
||||||
createPreviewer();
|
|
||||||
updatePreviewer(data);
|
|
||||||
}
|
|
||||||
enabled = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} [fn] - preprocessor
|
|
||||||
*/
|
|
||||||
init(fn) {
|
|
||||||
preprocess = fn;
|
|
||||||
},
|
|
||||||
|
|
||||||
update(newData) {
|
|
||||||
data = newData;
|
|
||||||
if (!port) {
|
|
||||||
if (!data.id || !data.enabled || !enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createPreviewer();
|
|
||||||
}
|
|
||||||
updatePreviewer(data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createPreviewer() {
|
|
||||||
port = chrome.runtime.connect({name: 'livePreview'});
|
|
||||||
port.onDisconnect.addListener(err => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
el = $('#preview-errors');
|
|
||||||
el.onclick = () => messageBoxProxy.alert(el.title, 'pre');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePreviewer(data) {
|
|
||||||
try {
|
|
||||||
port.postMessage(preprocess ? await preprocess(data) : data);
|
|
||||||
el.hidden = true;
|
|
||||||
} catch (err) {
|
|
||||||
if (Array.isArray(err)) {
|
|
||||||
err = err.map(e => e.message || e).join('\n');
|
|
||||||
} else if (err && err.index != null) {
|
|
||||||
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
|
|
||||||
const pos = editor.getEditors()[0].posFromIndex(err.index);
|
|
||||||
err.message = `${pos.line}:${pos.ch} ${err.message || err}`;
|
|
||||||
}
|
|
||||||
el.title = err.message || `${err}`;
|
|
||||||
el.hidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
257
edit/beautify.js
257
edit/beautify.js
|
@ -1,174 +1,107 @@
|
||||||
/* global $ $create moveFocus */// dom.js
|
/*
|
||||||
/* global CodeMirror */
|
global CodeMirror loadScript css_beautify
|
||||||
/* global createHotkeyInput helpPopup */// util.js
|
global editors getSectionForChild showHelp
|
||||||
/* global editor */
|
*/
|
||||||
/* global prefs */
|
|
||||||
/* global t */// localization.js
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
CodeMirror.commands.beautify = cm => {
|
function beautify(event) {
|
||||||
// using per-section mode when code editor or applies-to block is focused
|
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
||||||
const isPerSection = cm.display.wrapper.parentElement.contains(document.activeElement);
|
.then(() => {
|
||||||
beautify(isPerSection ? [cm] : editor.getEditors(), false);
|
if (!window.css_beautify && window.exports) {
|
||||||
};
|
window.css_beautify = window.exports.css_beautify;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(doBeautify);
|
||||||
|
|
||||||
prefs.subscribe('editor.beautify.hotkey', (key, value) => {
|
function doBeautify() {
|
||||||
const {extraKeys} = CodeMirror.defaults;
|
const tabs = prefs.get('editor.indentWithTabs');
|
||||||
for (const [key, cmd] of Object.entries(extraKeys)) {
|
const options = prefs.get('editor.beautify');
|
||||||
if (cmd === 'beautify') {
|
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
||||||
delete extraKeys[key];
|
options.indent_char = tabs ? '\t' : ' ';
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
extraKeys[value] = 'beautify';
|
|
||||||
}
|
|
||||||
}, {runNow: true});
|
|
||||||
|
|
||||||
/**
|
const section = getSectionForChild(event.target);
|
||||||
* @name beautify
|
const scope = section ? [section.CodeMirror] : editors;
|
||||||
* @param {CodeMirror[]} scope
|
|
||||||
* @param {boolean} [ui=true]
|
|
||||||
*/
|
|
||||||
async function beautify(scope, ui = true) {
|
|
||||||
await require(['/vendor-overwrites/beautify/beautify-css-mod']); /* global css_beautify */
|
|
||||||
const tabs = prefs.get('editor.indentWithTabs');
|
|
||||||
const options = Object.assign(prefs.defaults['editor.beautify'], prefs.get('editor.beautify'));
|
|
||||||
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
|
||||||
options.indent_char = tabs ? '\t' : ' ';
|
|
||||||
if (ui) {
|
|
||||||
createBeautifyUI(scope, options);
|
|
||||||
}
|
|
||||||
for (const cm of scope) {
|
|
||||||
setTimeout(beautifyEditor, 0, cm, options, ui);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function beautifyEditor(cm, options, ui) {
|
showHelp(t('styleBeautify'), '<div class="beautify-options">' +
|
||||||
const pos = options.translate_positions =
|
optionHtml('.selector1,', 'selector_separator_newline') +
|
||||||
[].concat.apply([], cm.doc.sel.ranges.map(r =>
|
optionHtml('.selector2,', 'newline_before_open_brace') +
|
||||||
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
|
optionHtml('{', 'newline_after_open_brace') +
|
||||||
const text = cm.getValue();
|
optionHtml('border: none;', 'newline_between_properties', true) +
|
||||||
const newText = css_beautify(text, options);
|
optionHtml('display: block;', 'newline_before_close_brace', true) +
|
||||||
if (newText !== text) {
|
optionHtml('}', 'newline_between_rules') +
|
||||||
if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
|
`<label style="display: block; clear: both;">
|
||||||
// clear the list if last change wasn't a css-beautify
|
<input data-option="indent_conditional" type="checkbox"
|
||||||
cm.beautifyChange = {};
|
${options.indent_conditional !== false ? 'checked' : ''}>
|
||||||
}
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>` +
|
||||||
cm.setValue(newText);
|
t('styleBeautifyIndentConditional') + '</label>' +
|
||||||
const selections = [];
|
'</div>' +
|
||||||
for (let i = 0; i < pos.length; i += 2) {
|
'<div><button role="undo"></button></div>');
|
||||||
selections.push({anchor: pos[i], head: pos[i + 1]});
|
|
||||||
}
|
|
||||||
const {scrollX, scrollY} = window;
|
|
||||||
cm.setSelections(selections);
|
|
||||||
window.scrollTo(scrollX, scrollY);
|
|
||||||
cm.beautifyChange[cm.changeGeneration()] = true;
|
|
||||||
if (ui) {
|
|
||||||
$('button[role="close"]', helpPopup.div).disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBeautifyUI(scope, options) {
|
$('#help-popup').className = 'wide';
|
||||||
helpPopup.show(t('styleBeautify'),
|
|
||||||
$create([
|
const undoButton = $('#help-popup button[role="undo"]');
|
||||||
$create('.beautify-options', [
|
undoButton.textContent = t(scope.length === 1 ? 'undo' : 'undoGlobal');
|
||||||
$createOption('.selector1,', 'selector_separator_newline'),
|
undoButton.addEventListener('click', () => {
|
||||||
$createOption('.selector2', 'newline_before_open_brace'),
|
let undoable = false;
|
||||||
$createOption('{', 'newline_after_open_brace'),
|
scope.forEach(cm => {
|
||||||
$createOption('border: none;', 'newline_between_properties', true),
|
if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
|
||||||
$createOption('display: block;', 'newline_before_close_brace', true),
|
delete cm.beautifyChange[cm.changeGeneration()];
|
||||||
$createOption('}', 'newline_between_rules'),
|
cm.undo();
|
||||||
$createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'),
|
cm.scrollIntoView(cm.getCursor());
|
||||||
$createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'),
|
undoable |= cm.beautifyChange[cm.changeGeneration()];
|
||||||
editor.isUsercss && $createLabeledCheckbox('indent_mozdoc', '', '... @-moz-document'),
|
}
|
||||||
]),
|
});
|
||||||
$create('p.beautify-hint', [
|
undoButton.disabled = !undoable;
|
||||||
$create('span', t('styleBeautifyHint') + '\u00A0'),
|
|
||||||
createHotkeyInput('editor.beautify.hotkey', {
|
|
||||||
buttons: false,
|
|
||||||
onDone: () => moveFocus(helpPopup.div, 0),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
$create('.buttons', [
|
|
||||||
$create('button', {
|
|
||||||
attributes: {role: 'close'},
|
|
||||||
onclick: helpPopup.close,
|
|
||||||
}, t('confirmClose')),
|
|
||||||
$create('button', {
|
|
||||||
attributes: {role: 'undo'},
|
|
||||||
onclick() {
|
|
||||||
let undoable = false;
|
|
||||||
for (const cm of scope) {
|
|
||||||
const data = cm.beautifyChange;
|
|
||||||
if (!data || !data[cm.changeGeneration()]) continue;
|
|
||||||
delete data[cm.changeGeneration()];
|
|
||||||
const {scrollX, scrollY} = window;
|
|
||||||
cm.undo();
|
|
||||||
cm.scrollIntoView(cm.getCursor());
|
|
||||||
window.scrollTo(scrollX, scrollY);
|
|
||||||
undoable |= data[cm.changeGeneration()];
|
|
||||||
}
|
|
||||||
this.disabled = !undoable;
|
|
||||||
},
|
|
||||||
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
{
|
|
||||||
className: 'wide',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.beautify-options').onchange = ({target}) => {
|
scope.forEach(cm => {
|
||||||
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
|
setTimeout(() => {
|
||||||
const elLine = target.closest('[newline]');
|
const pos = options.translate_positions =
|
||||||
if (elLine) elLine.setAttribute('newline', value);
|
[].concat.apply([], cm.doc.sel.ranges.map(r =>
|
||||||
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
|
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
|
||||||
beautify(scope, false);
|
const text = cm.getValue();
|
||||||
};
|
const newText = css_beautify(text, options);
|
||||||
|
if (newText !== text) {
|
||||||
|
if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
|
||||||
|
// clear the list if last change wasn't a css-beautify
|
||||||
|
cm.beautifyChange = {};
|
||||||
|
}
|
||||||
|
cm.setValue(newText);
|
||||||
|
const selections = [];
|
||||||
|
for (let i = 0; i < pos.length; i += 2) {
|
||||||
|
selections.push({anchor: pos[i], head: pos[i + 1]});
|
||||||
|
}
|
||||||
|
cm.setSelections(selections);
|
||||||
|
cm.beautifyChange[cm.changeGeneration()] = true;
|
||||||
|
undoButton.disabled = false;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
function $createOption(label, optionName, indent) {
|
$('.beautify-options').onchange = ({target}) => {
|
||||||
const value = options[optionName];
|
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
|
||||||
return (
|
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
|
||||||
$create('div', {attributes: {newline: value}}, [
|
if (target.parentNode.hasAttribute('newline')) {
|
||||||
$create('span', indent ? {attributes: {indent: ''}} : {}, label),
|
target.parentNode.setAttribute('newline', value.toString());
|
||||||
$create('div.select-resizer', [
|
}
|
||||||
$create('select', {dataset: {option: optionName}}, [
|
doBeautify();
|
||||||
$create('option', {selected: !value}, '\xA0'),
|
};
|
||||||
$create('option', {selected: value}, '\\n'),
|
|
||||||
]),
|
|
||||||
$create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
|
|
||||||
$create('SVG:path', {
|
|
||||||
'fill-rule': 'evenodd',
|
|
||||||
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
|
|
||||||
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z',
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function $createLabeledCheckbox(optionName, i18nKey, text) {
|
function optionHtml(label, optionName, indent) {
|
||||||
return (
|
const value = options[optionName];
|
||||||
$create('label', {style: 'display: block; clear: both;'}, [
|
return '<div newline="' + value.toString() + '">' +
|
||||||
$create('input', {
|
'<span' + (indent ? ' indent' : '') + '>' + label + '</span>' +
|
||||||
type: 'checkbox',
|
'<div class="select-resizer">' +
|
||||||
dataset: {option: optionName},
|
'<select data-option="' + optionName + '">' +
|
||||||
checked: options[optionName] !== false,
|
'<option' + (value ? '' : ' selected') + '> </option>' +
|
||||||
}),
|
'<option' + (value ? ' selected' : '') + '>\\n</option>' +
|
||||||
$create('SVG:svg.svg-icon.checked',
|
'</select>' +
|
||||||
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
|
'<svg class="svg-icon select-arrow" viewBox="0 0 1792 1792">' +
|
||||||
i18nKey ? t(i18nKey) : text,
|
'<path fill-rule="evenodd" d="M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
|
||||||
])
|
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"/>' +
|
||||||
);
|
'</svg>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* exported initBeautifyButton */
|
|
||||||
function initBeautifyButton(btn, scope) {
|
|
||||||
btn.onclick = btn.oncontextmenu = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
beautify(scope || editor.getEditors(), e.type === 'click');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,33 +1,29 @@
|
||||||
/* Built-in CodeMirror and addon customization */
|
|
||||||
|
|
||||||
.CodeMirror-hints {
|
.CodeMirror-hints {
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
.CodeMirror-hint:hover {
|
.CodeMirror-hint:hover {
|
||||||
color: var(--bg);
|
color: white;
|
||||||
background: #08f;
|
background: #08f;
|
||||||
}
|
}
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
border: solid var(--c80) 1px;
|
border: solid #CCC 1px;
|
||||||
transition: box-shadow .1s;
|
|
||||||
}
|
}
|
||||||
.CodeMirror {
|
.CodeMirror-lint-mark-warning {
|
||||||
color: inherit;
|
background: none;
|
||||||
background-color: inherit;
|
|
||||||
border: solid var(--c80) 1px;
|
|
||||||
transition: box-shadow .1s;
|
|
||||||
}
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
background-color: var(--c95);
|
|
||||||
border-color: var(--c85);
|
|
||||||
}
|
|
||||||
#stylus#stylus .CodeMirror {
|
|
||||||
/* Using a specificity hack to override userstyles */
|
|
||||||
/* Not using the ring-color hack as it became ugly in new Chrome */
|
|
||||||
outline: none !important;
|
|
||||||
}
|
}
|
||||||
.CodeMirror-dialog {
|
.CodeMirror-dialog {
|
||||||
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
-webkit-animation: highlight 3s ease-out;
|
||||||
|
}
|
||||||
|
.CodeMirror-focused {
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
@supports (-moz-appearance:none) {
|
||||||
|
/* restrict to FF */
|
||||||
|
.CodeMirror-focused {
|
||||||
|
outline: #7dadd9 auto 1px;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.CodeMirror-search-field {
|
.CodeMirror-search-field {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
|
@ -36,8 +32,20 @@
|
||||||
width: 5em;
|
width: 5em;
|
||||||
}
|
}
|
||||||
.CodeMirror-search-hint {
|
.CodeMirror-search-hint {
|
||||||
color: var(--c50);
|
color: #888;
|
||||||
}
|
}
|
||||||
|
.cm-uso-variable {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.cm-searching.cm-matchhighlight {
|
||||||
|
/* tokens found by manual search should not animate by cm-matchhighlight */
|
||||||
|
animation-name: search-and-match-highlighter !important;
|
||||||
|
}
|
||||||
|
@keyframes search-and-match-highlighter {
|
||||||
|
from { background-color: rgba(255, 255, 0, .4); } /* search color */
|
||||||
|
to { background-color: rgba(100, 255, 100, .4); } /* sarch + highlight */
|
||||||
|
}
|
||||||
|
|
||||||
.CodeMirror-activeline .applies-to:before {
|
.CodeMirror-activeline .applies-to:before {
|
||||||
background-color: hsla(214, 100%, 90%, 0.15);
|
background-color: hsla(214, 100%, 90%, 0.15);
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -48,101 +56,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-activeline .applies-to ul {
|
.CodeMirror-activeline .applies-to ul {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.CodeMirror-foldgutter-open::after,
|
|
||||||
.CodeMirror-foldgutter-folded::after {
|
|
||||||
top: 5px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
border-style: solid;
|
|
||||||
opacity: .5;
|
|
||||||
left: 1px;
|
|
||||||
}
|
|
||||||
.CodeMirror-foldgutter-open::after {
|
|
||||||
border-width: 5px 3px 0 3px;
|
|
||||||
border-color: currentColor transparent transparent transparent;
|
|
||||||
}
|
|
||||||
.CodeMirror-foldgutter-folded::after {
|
|
||||||
margin-top: -2px;
|
|
||||||
margin-left: 1px;
|
|
||||||
border-width: 4px 0 4px 5px;
|
|
||||||
border-color: transparent transparent transparent currentColor;
|
|
||||||
}
|
|
||||||
.CodeMirror-linenumber {
|
|
||||||
cursor: pointer; /* for bookmarking */
|
|
||||||
}
|
|
||||||
.cm-matchhighlight,
|
|
||||||
.CodeMirror-selection-highlight-scrollbar {
|
|
||||||
background: hsla(200, 100%, 50%, var(--match-hl-opacity, .1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom stuff we add to CodeMirror */
|
|
||||||
|
|
||||||
.cm-uso-variable {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.gutter-bookmark {
|
|
||||||
background: linear-gradient(0deg, hsla(180, 100%, 30%, .75) 2px, hsla(180, 100%, 30%, .2) 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (prefers-color-scheme: dark), dark {
|
|
||||||
.CodeMirror {
|
|
||||||
--match-hl-opacity: .18;
|
|
||||||
}
|
|
||||||
.CodeMirror-dialog {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
.CodeMirror-dialog-top {
|
|
||||||
border-color: #555;
|
|
||||||
}
|
|
||||||
.CodeMirror-activeline-background {
|
|
||||||
background: hsl(180, 21%, 18%);
|
|
||||||
}
|
|
||||||
.CodeMirror-selected,
|
|
||||||
.CodeMirror-focused .CodeMirror-selected,
|
|
||||||
.CodeMirror-line::selection,
|
|
||||||
.CodeMirror-line > span::selection,
|
|
||||||
.CodeMirror-line > span > span::selection {
|
|
||||||
background: #444;
|
|
||||||
}
|
|
||||||
.CodeMirror-line::-moz-selection,
|
|
||||||
.CodeMirror-line > span::-moz-selection,
|
|
||||||
.CodeMirror-line > span > span::-moz-selection {
|
|
||||||
/* TODO: remove this when strict_min_version >= 62 */
|
|
||||||
background: #444;
|
|
||||||
}
|
|
||||||
.cm-s-default div.CodeMirror-cursor {
|
|
||||||
border-left: 1px solid #fff;
|
|
||||||
}
|
|
||||||
/* Using Chromium's dark devtools colors */
|
|
||||||
.cm-s-default .cm-atom,
|
|
||||||
.cm-s-default .cm-number { color: #a1f7b5 }
|
|
||||||
.cm-s-default .cm-attribute { color: #6194c6 }
|
|
||||||
.cm-s-default .cm-bracket { color: #997 }
|
|
||||||
.cm-s-default .cm-builtin,
|
|
||||||
.cm-s-default .cm-link { color: #9fb4d6 }
|
|
||||||
.cm-s-default .cm-comment { color: #747474 }
|
|
||||||
.cm-s-default .cm-qualifier { color: #ffa34f }
|
|
||||||
.cm-s-default .cm-def,
|
|
||||||
.cm-s-default .cm-header,
|
|
||||||
.cm-s-default .cm-tag,
|
|
||||||
.cm-s-default .cm-type { color: #5db0d7 }
|
|
||||||
.cm-s-default .cm-hr { color: #999 }
|
|
||||||
.cm-s-default .cm-keyword { color: #9a7fd5 }
|
|
||||||
.cm-s-default .cm-meta { color: #ddfb55 }
|
|
||||||
.cm-s-default .cm-operator { color: #d2c057 }
|
|
||||||
.cm-s-default .cm-string { color: #f28b54 }
|
|
||||||
.cm-s-default .cm-variable { color: #d9d9d9 }
|
|
||||||
.cm-s-default .cm-variable-2 { color: #72b9ff }
|
|
||||||
.cm-s-default .cm-variable-3 { color: #9bbbdc }
|
|
||||||
|
|
||||||
@keyframes highlight {
|
|
||||||
from {
|
|
||||||
background-color: #888;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
/* global $ */// dom.js
|
/* global CodeMirror prefs loadScript editor */
|
||||||
/* global CodeMirror */
|
|
||||||
/* global UA */// toolbox.js
|
|
||||||
/* global editor */
|
|
||||||
/* global prefs */
|
|
||||||
/* global t */// localization.js
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(function () {
|
||||||
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
||||||
if (!prefs.get('editor.keyMap')) {
|
if (!prefs.get('editor.keyMap')) {
|
||||||
prefs.reset('editor.keyMap');
|
prefs.reset('editor.keyMap');
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
|
||||||
mode: 'css',
|
mode: 'css',
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
lineWrapping: prefs.get('editor.lineWrapping'),
|
||||||
|
@ -24,125 +19,187 @@
|
||||||
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
||||||
],
|
],
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
|
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
|
||||||
hintOptions: {},
|
hintOptions: {},
|
||||||
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
||||||
styleActiveLine: {nonEmpty: true},
|
styleActiveLine: true,
|
||||||
theme: prefs.get('editor.theme'),
|
theme: 'default',
|
||||||
keyMap: prefs.get('editor.keyMap'),
|
keyMap: prefs.get('editor.keyMap'),
|
||||||
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
||||||
// independent of current keyMap; some are implemented only for the edit page
|
// independent of current keyMap
|
||||||
'Alt-Enter': 'toggleStyle',
|
'Alt-Enter': 'toggleStyle',
|
||||||
'Alt-PageDown': 'nextEditor',
|
'Alt-PageDown': 'nextEditor',
|
||||||
'Alt-PageUp': 'prevEditor',
|
'Alt-PageUp': 'prevEditor'
|
||||||
'Ctrl-Pause': 'toggleEditorFocus',
|
|
||||||
}),
|
}),
|
||||||
maxHighlightLength: 100e3,
|
maxHighlightLength: 100e3,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
||||||
|
|
||||||
// Adding hotkeys to some keymaps except 'basic' which is primitive by design
|
CodeMirror.commands.blockComment = cm => {
|
||||||
{
|
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||||
const KM = CodeMirror.keyMap;
|
};
|
||||||
const extras = Object.values(CodeMirror.defaults.extraKeys);
|
|
||||||
if (!extras.includes('jumpToLine')) {
|
// 'basic' keymap only has basic keys by design, so we skip it
|
||||||
KM.sublime['Ctrl-G'] = 'jumpToLine';
|
|
||||||
KM.emacsy['Ctrl-G'] = 'jumpToLine';
|
const extraKeysCommands = {};
|
||||||
KM.pcDefault['Ctrl-J'] = 'jumpToLine';
|
Object.keys(CodeMirror.defaults.extraKeys).forEach(key => {
|
||||||
KM.macDefault['Cmd-J'] = 'jumpToLine';
|
extraKeysCommands[CodeMirror.defaults.extraKeys[key]] = true;
|
||||||
}
|
});
|
||||||
if (!extras.includes('autocomplete')) {
|
if (!extraKeysCommands.jumpToLine) {
|
||||||
// will be used by 'sublime' on PC via fallthrough
|
CodeMirror.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
|
||||||
KM.pcDefault['Ctrl-Space'] = 'autocomplete';
|
CodeMirror.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
|
||||||
// OSX uses Ctrl-Space and Cmd-Space for something else
|
CodeMirror.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
|
||||||
KM.macDefault['Alt-Space'] = 'autocomplete';
|
CodeMirror.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
|
||||||
// copied from 'emacs' keymap
|
}
|
||||||
KM.emacsy['Alt-/'] = 'autocomplete';
|
if (!extraKeysCommands.autocomplete) {
|
||||||
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
// will be used by 'sublime' on PC via fallthrough
|
||||||
}
|
CodeMirror.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete';
|
||||||
if (!extras.includes('blockComment')) {
|
// OSX uses Ctrl-Space and Cmd-Space for something else
|
||||||
KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
|
CodeMirror.keyMap.macDefault['Alt-Space'] = 'autocomplete';
|
||||||
}
|
// copied from 'emacs' keymap
|
||||||
if (UA.windows) {
|
CodeMirror.keyMap.emacsy['Alt-/'] = 'autocomplete';
|
||||||
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
|
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
||||||
if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
|
}
|
||||||
if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
|
if (!extraKeysCommands.blockComment) {
|
||||||
if (!extras.includes('replace')) KM.pcDefault['Ctrl-R'] = 'replace';
|
CodeMirror.keyMap.sublime['Shift-Ctrl-/'] = 'blockComment';
|
||||||
// try to remap non-interceptable (Shift-)Ctrl-N/T/W hotkeys
|
|
||||||
// Note: modifier order in CodeMirror is S-C-A
|
|
||||||
for (const char of ['N', 'T', 'W']) {
|
|
||||||
for (const remap of [
|
|
||||||
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
|
||||||
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
|
|
||||||
]) {
|
|
||||||
const oldKey = remap.from + char;
|
|
||||||
for (const km of Object.values(KM)) {
|
|
||||||
const command = km[oldKey];
|
|
||||||
if (!command) continue;
|
|
||||||
for (const newMod of remap.to) {
|
|
||||||
const newKey = newMod + char;
|
|
||||||
if (newKey in km) continue;
|
|
||||||
km[newKey] = command;
|
|
||||||
delete km[oldKey];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(CodeMirror.prototype, {
|
if (navigator.appVersion.includes('Windows')) {
|
||||||
/**
|
// 'pcDefault' keymap on Windows should have F3/Shift-F3
|
||||||
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
|
if (!extraKeysCommands.findNext) {
|
||||||
* @param {boolean} [force]
|
CodeMirror.keyMap.pcDefault['F3'] = 'findNext';
|
||||||
*/
|
}
|
||||||
setPreprocessor(pp, force) {
|
if (!extraKeysCommands.findPrev) {
|
||||||
const name = pp === 'less' ? 'text/x-less' : pp === 'stylus' ? pp : 'css';
|
CodeMirror.keyMap.pcDefault['Shift-F3'] = 'findPrev';
|
||||||
const m = this.doc.mode;
|
}
|
||||||
if (force || (m.helperType ? m.helperType !== pp : m.name !== name)) {
|
|
||||||
this.setOption('mode', name);
|
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
|
||||||
this.doc.mode.lineComment = ''; // stylelint chokes on line comments a lot
|
['N', 'T', 'W'].forEach(char => {
|
||||||
}
|
[
|
||||||
},
|
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
||||||
/** Superfast GC-friendly check that runs until the first non-space line */
|
// Note: modifier order in CodeMirror is S-C-A
|
||||||
isBlank() {
|
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']}
|
||||||
let filled;
|
].forEach(remap => {
|
||||||
this.eachLine(({text}) => (filled = text && /\S/.test(text)));
|
const oldKey = remap.from + char;
|
||||||
return !filled;
|
Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
|
||||||
},
|
const keyMap = CodeMirror.keyMap[keyMapName];
|
||||||
/**
|
const command = keyMap[oldKey];
|
||||||
* Sets cursor and centers it in view if `pos` was out of view
|
if (!command) {
|
||||||
* @param {CodeMirror.Pos} pos
|
return;
|
||||||
* @param {CodeMirror.Pos} [end] - will set a selection from `pos` to `end`
|
}
|
||||||
*/
|
remap.to.some(newMod => {
|
||||||
jumpToPos(pos, end = pos) {
|
const newKey = newMod + char;
|
||||||
const {curOp} = this;
|
if (!(newKey in keyMap)) {
|
||||||
if (!curOp) this.startOperation();
|
delete keyMap[oldKey];
|
||||||
const y = this.cursorCoords(pos, 'window').top;
|
keyMap[newKey] = command;
|
||||||
const rect = this.display.wrapper.getBoundingClientRect();
|
return true;
|
||||||
// case 1) outside of CM viewport or too close to edge so tell CM to render a new viewport
|
}
|
||||||
if (y < rect.top + 50 || y > rect.bottom - 100) {
|
});
|
||||||
this.scrollIntoView(pos, rect.height / 2);
|
});
|
||||||
// case 2) inside CM viewport but outside of window viewport so just scroll the window
|
});
|
||||||
} else if (y < 0 || y > innerHeight) {
|
});
|
||||||
editor.scrollToEditor(this);
|
}
|
||||||
}
|
|
||||||
// Using prototype since our bookmark patch sets cm.setSelection to jumpToPos
|
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
|
||||||
CodeMirror.prototype.setSelection.call(this, pos, end);
|
'mix-blend-mode': true,
|
||||||
if (!curOp) this.endOperation();
|
'isolation': true,
|
||||||
},
|
});
|
||||||
|
Object.assign(CodeMirror.mimeModes['text/css'].valueKeywords, {
|
||||||
|
'isolate': true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign(CodeMirror.commands, {
|
const MODE = {
|
||||||
jumpToLine(cm) {
|
stylus: 'stylus',
|
||||||
const cur = cm.getCursor();
|
uso: 'css'
|
||||||
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
};
|
||||||
if (oldDialog) cm.focus(); // close the currently opened minidialog
|
|
||||||
cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
|
CodeMirror.defineExtension('setPreprocessor', function (preprocessor, force = false) {
|
||||||
const [line, ch] = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$|$/);
|
const mode = MODE[preprocessor] || 'css';
|
||||||
if (line) cm.setCursor(line - 1, ch ? ch - 1 : cur.ch);
|
if ((this.doc.mode || {}).name === mode && !force) {
|
||||||
}, {value: cur.line + 1});
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
|
if (mode === 'css') {
|
||||||
|
this.setOption('mode', mode);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return loadScript(`/vendor/codemirror/mode/${mode}/${mode}.js`).then(() => {
|
||||||
|
this.setOption('mode', mode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('isBlank', function () {
|
||||||
|
// superfast checking as it runs only until the first non-blank line
|
||||||
|
let isBlank = true;
|
||||||
|
this.doc.eachLine(line => {
|
||||||
|
if (line.text && line.text.trim()) {
|
||||||
|
isBlank = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return isBlank;
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
CodeMirror.hint && (() => {
|
||||||
|
const USO_VAR = 'uso-variable';
|
||||||
|
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
|
||||||
|
const USO_INVALID_VAR = 'error ' + USO_VAR;
|
||||||
|
|
||||||
|
const originalHelper = CodeMirror.hint.css || (() => {});
|
||||||
|
CodeMirror.registerHelper('hint', 'css', function (cm) {
|
||||||
|
const {line, ch} = cm.getCursor();
|
||||||
|
const {styles, text} = cm.getLineHandle(line);
|
||||||
|
if (!styles || !editor) {
|
||||||
|
return originalHelper(cm);
|
||||||
|
}
|
||||||
|
let prev = 0;
|
||||||
|
for (let i = 1; i < styles.length; i += 2) {
|
||||||
|
let end = styles[i];
|
||||||
|
if (prev <= ch && ch <= end &&
|
||||||
|
(styles[i + 1] || '').includes(USO_VAR)) {
|
||||||
|
const adjust = text[prev] === '/' ? 4 : 0;
|
||||||
|
prev += adjust;
|
||||||
|
end -= adjust;
|
||||||
|
const leftPart = text.slice(prev, ch);
|
||||||
|
const list = Object.keys(editor.getStyle().usercssData.vars)
|
||||||
|
.filter(name => name.startsWith(leftPart));
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
from: {line, ch: prev},
|
||||||
|
to: {line, ch: end},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
prev = end;
|
||||||
|
}
|
||||||
|
return originalHelper(cm);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks;
|
||||||
|
const originalCommentHook = hooks['/'];
|
||||||
|
hooks['/'] = tokenizeUsoVariables;
|
||||||
|
|
||||||
|
function tokenizeUsoVariables(stream) {
|
||||||
|
const token = originalCommentHook.apply(this, arguments);
|
||||||
|
if (token[1] !== 'comment') {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
const {string, start, pos} = stream;
|
||||||
|
// /*[[install-key]]*/
|
||||||
|
// 01234 43210
|
||||||
|
if (string[start + 2] === '[' &&
|
||||||
|
string[start + 3] === '[' &&
|
||||||
|
string[pos - 3] === ']' &&
|
||||||
|
string[pos - 4] === ']') {
|
||||||
|
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
|
||||||
|
const name = vars && string.slice(start + 4, pos - 4);
|
||||||
|
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
|
||||||
|
token[0] = USO_VALID_VAR;
|
||||||
|
} else {
|
||||||
|
token[0] = USO_INVALID_VAR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
705
edit/codemirror-editing-hooks.js
Normal file
705
edit/codemirror-editing-hooks.js
Normal file
|
@ -0,0 +1,705 @@
|
||||||
|
/*
|
||||||
|
global CodeMirror linterConfig loadScript
|
||||||
|
global editors editor styleId
|
||||||
|
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
|
|
||||||
|
CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
|
||||||
|
|
||||||
|
const COMMANDS = {
|
||||||
|
save,
|
||||||
|
toggleStyle,
|
||||||
|
jumpToLine,
|
||||||
|
nextEditor, prevEditor,
|
||||||
|
find, findNext, findPrev, replace, replaceAll,
|
||||||
|
};
|
||||||
|
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||||
|
const REROUTED = new Set([
|
||||||
|
...Object.keys(COMMANDS),
|
||||||
|
'colorpicker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ORIGINAL_COMMAND = {
|
||||||
|
find: CodeMirror.commands.find,
|
||||||
|
findNext: CodeMirror.commands.findNext,
|
||||||
|
findPrev: CodeMirror.commands.findPrev,
|
||||||
|
replace: CodeMirror.commands.replace
|
||||||
|
};
|
||||||
|
const ORIGINAL_METHOD = {
|
||||||
|
openDialog: CodeMirror.prototype.openDialog,
|
||||||
|
openConfirm: CodeMirror.prototype.openConfirm,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(CodeMirror, {
|
||||||
|
getOption,
|
||||||
|
setOption,
|
||||||
|
});
|
||||||
|
Object.assign(CodeMirror.commands,
|
||||||
|
COMMANDS);
|
||||||
|
Object.assign(CodeMirror.prototype, {
|
||||||
|
getSection,
|
||||||
|
rerouteHotkeys,
|
||||||
|
});
|
||||||
|
|
||||||
|
// cm.state.search for last used 'find'
|
||||||
|
let searchState;
|
||||||
|
|
||||||
|
onDOMready().then(() => {
|
||||||
|
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
|
||||||
|
showKeyInSaveButtonTooltip();
|
||||||
|
|
||||||
|
// N.B. the event listener should be registered before setupLivePrefs()
|
||||||
|
$('#options').addEventListener('change', onOptionElementChanged);
|
||||||
|
buildOptionsElements();
|
||||||
|
|
||||||
|
rerouteHotkeys(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function getOption(o) {
|
||||||
|
return CodeMirror.defaults[o];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOption(o, v) {
|
||||||
|
CodeMirror.defaults[o] = v;
|
||||||
|
if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
|
||||||
|
throttleSetOption({key: o, value: v, index: 0});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editors.forEach(editor => {
|
||||||
|
editor.setOption(o, v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function throttleSetOption({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
timeStart = performance.now(),
|
||||||
|
cmStart = editors.lastActive || editors[0],
|
||||||
|
editorsCopy = editors.slice(),
|
||||||
|
progress,
|
||||||
|
}) {
|
||||||
|
if (index === 0) {
|
||||||
|
if (!cmStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmStart.setOption(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const THROTTLE_AFTER_MS = 100;
|
||||||
|
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
||||||
|
|
||||||
|
const t0 = performance.now();
|
||||||
|
const total = editorsCopy.length;
|
||||||
|
while (index < total) {
|
||||||
|
const cm = editorsCopy[index++];
|
||||||
|
if (cm === cmStart ||
|
||||||
|
cm !== editors[index] && !editors.includes(cm)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cm.setOption(key, value);
|
||||||
|
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= total) {
|
||||||
|
$.remove(progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!progress &&
|
||||||
|
index < total / 2 &&
|
||||||
|
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
||||||
|
let option = $('#editor.' + key);
|
||||||
|
if (option) {
|
||||||
|
if (option.type === 'checkbox') {
|
||||||
|
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
||||||
|
}
|
||||||
|
progress = document.body.appendChild(
|
||||||
|
$create('.set-option-progress', {targetElement: option}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (progress) {
|
||||||
|
const optionBounds = progress.targetElement.getBoundingClientRect();
|
||||||
|
const bounds = {
|
||||||
|
top: optionBounds.top + window.scrollY + 1,
|
||||||
|
left: optionBounds.left + window.scrollX + 1,
|
||||||
|
width: (optionBounds.width - 2) * index / total | 0,
|
||||||
|
height: optionBounds.height - 2,
|
||||||
|
};
|
||||||
|
const style = progress.style;
|
||||||
|
for (const prop in bounds) {
|
||||||
|
if (bounds[prop] !== parseFloat(style[prop])) {
|
||||||
|
style[prop] = bounds[prop] + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(throttleSetOption, 0, {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
timeStart,
|
||||||
|
cmStart,
|
||||||
|
editorsCopy,
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSection() {
|
||||||
|
return this.display.wrapper.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextEditor(cm) {
|
||||||
|
return nextPrevEditor(cm, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevEditor(cm) {
|
||||||
|
return nextPrevEditor(cm, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPrevEditor(cm, direction) {
|
||||||
|
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
||||||
|
makeSectionVisible(cm);
|
||||||
|
cm.focus();
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpToLine(cm) {
|
||||||
|
const cur = cm.getCursor();
|
||||||
|
refocusMinidialog(cm);
|
||||||
|
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
||||||
|
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||||
|
if (m) {
|
||||||
|
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||||
|
}
|
||||||
|
}, {value: cur.line + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refocusMinidialog(cm) {
|
||||||
|
const section = cm.getSection();
|
||||||
|
if (!$('.CodeMirror-dialog', section)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// close the currently opened minidialog
|
||||||
|
cm.focus();
|
||||||
|
// make sure to focus the input in newly opened minidialog
|
||||||
|
setTimeout(() => {
|
||||||
|
$('.CodeMirror-dialog', section).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOptionElementChanged(event) {
|
||||||
|
const el = event.target;
|
||||||
|
let option = el.id.replace(/^editor\./, '');
|
||||||
|
if (!option) {
|
||||||
|
console.error('no "cm_option"', el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value = el.type === 'checkbox' ? el.checked : el.value;
|
||||||
|
switch (option) {
|
||||||
|
case 'tabSize':
|
||||||
|
value = Number(value);
|
||||||
|
CodeMirror.setOption('indentUnit', value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'theme': {
|
||||||
|
const themeLink = $('#cm-theme');
|
||||||
|
// use non-localized 'default' internally
|
||||||
|
if (!value || value === 'default' || value === t('defaultTheme')) {
|
||||||
|
value = 'default';
|
||||||
|
if (prefs.get(el.id) !== value) {
|
||||||
|
prefs.set(el.id, value);
|
||||||
|
}
|
||||||
|
themeLink.href = '';
|
||||||
|
el.selectedIndex = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
||||||
|
if (themeLink.href === url) {
|
||||||
|
// preloaded in initCodeMirror()
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||||
|
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
|
||||||
|
setTimeout(() => {
|
||||||
|
CodeMirror.setOption(option, value);
|
||||||
|
themeLink.remove();
|
||||||
|
$('#cm-theme2').id = 'cm-theme';
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'autocompleteOnTyping':
|
||||||
|
editors.forEach(cm => setupAutocomplete(cm, el.checked));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'autoCloseBrackets':
|
||||||
|
Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
|
||||||
|
CodeMirror.setOption(option, value);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'matchHighlight':
|
||||||
|
switch (value) {
|
||||||
|
case 'token':
|
||||||
|
case 'selection':
|
||||||
|
document.body.dataset[option] = value;
|
||||||
|
value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
option = 'highlightSelectionMatches';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'colorpicker':
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CodeMirror.setOption(option, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOptionsElements() {
|
||||||
|
// no need to escape the period in the id
|
||||||
|
const themeControl = $('#editor.theme');
|
||||||
|
const themeList = localStorage.codeMirrorThemes;
|
||||||
|
if (themeList) {
|
||||||
|
optionsFromArray(themeControl, themeList.split(/\s+/));
|
||||||
|
} else {
|
||||||
|
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||||
|
const theme = prefs.get('editor.theme');
|
||||||
|
optionsFromArray(themeControl, [theme === 'default' ? t('defaultTheme') : theme]);
|
||||||
|
getCodeMirrorThemes().then(() => {
|
||||||
|
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||||
|
optionsFromArray(themeControl, themes);
|
||||||
|
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
optionsFromArray($('#editor.keyMap'), Object.keys(CodeMirror.keyMap).sort());
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionsFromArray(parent, options) {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (const opt of options) {
|
||||||
|
fragment.appendChild($create('option', opt));
|
||||||
|
}
|
||||||
|
parent.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
function shouldIgnoreCase(query) {
|
||||||
|
// treat all-lowercase non-regexp queries as case-insensitive
|
||||||
|
return typeof query === 'string' && query === query.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateState(cm, newState) {
|
||||||
|
if (!newState) {
|
||||||
|
if (cm.state.search) {
|
||||||
|
return cm.state.search;
|
||||||
|
}
|
||||||
|
if (!searchState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
newState = searchState;
|
||||||
|
}
|
||||||
|
cm.state.search = {
|
||||||
|
query: newState.query,
|
||||||
|
overlay: newState.overlay,
|
||||||
|
annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
|
||||||
|
};
|
||||||
|
cm.addOverlay(newState.overlay);
|
||||||
|
return cm.state.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrides the original openDialog with a clone of the provided template
|
||||||
|
function customizeOpenDialog(cm, template, callback) {
|
||||||
|
cm.openDialog = (tmpl, cb, opt) => {
|
||||||
|
// invoke 'callback' and bind 'this' to the original callback
|
||||||
|
ORIGINAL_METHOD.openDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
|
||||||
|
};
|
||||||
|
setTimeout(() => (cm.openDialog = ORIGINAL_METHOD.openDialog));
|
||||||
|
refocusMinidialog(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusClosestCM(activeCM) {
|
||||||
|
editors.lastActive = activeCM;
|
||||||
|
const cm = getEditorInSight();
|
||||||
|
if (cm !== activeCM) {
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function find(activeCM) {
|
||||||
|
activeCM = focusClosestCM(activeCM);
|
||||||
|
customizeOpenDialog(activeCM, template.find, function (query) {
|
||||||
|
this(query);
|
||||||
|
searchState = activeCM.state.search;
|
||||||
|
const searchOthers = editors.length > 1 && searchState.query;
|
||||||
|
editors.forEach(cm => {
|
||||||
|
if (cm !== activeCM) {
|
||||||
|
cm.execCommand('clearSearch');
|
||||||
|
if (searchOthers) {
|
||||||
|
updateState(cm, searchState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (searchOthers && CodeMirror.cmpPos(searchState.posFrom, searchState.posTo) === 0) {
|
||||||
|
findNext(activeCM);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ORIGINAL_COMMAND.find(activeCM);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNext(activeCM, reverse) {
|
||||||
|
let state = updateState(activeCM);
|
||||||
|
if (!state || !state.query) {
|
||||||
|
find(activeCM);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pos = activeCM.getCursor(reverse ? 'from' : 'to');
|
||||||
|
// clear the selection, don't move the cursor
|
||||||
|
activeCM.setSelection(activeCM.getCursor());
|
||||||
|
|
||||||
|
const rxQuery = typeof state.query === 'object'
|
||||||
|
? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? 'i' : '');
|
||||||
|
|
||||||
|
if (
|
||||||
|
document.activeElement &&
|
||||||
|
document.activeElement.name === 'applies-value' &&
|
||||||
|
searchAppliesTo(activeCM)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cm = activeCM;
|
||||||
|
for (let i = 0; i < editors.length; i++) {
|
||||||
|
state = updateState(cm);
|
||||||
|
if (!cm.hasFocus()) {
|
||||||
|
pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
|
||||||
|
}
|
||||||
|
const searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
|
||||||
|
if (searchCursor.find(reverse)) {
|
||||||
|
if (editors.length > 1) {
|
||||||
|
makeSectionVisible(cm);
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
// speedup the original findNext
|
||||||
|
state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
|
||||||
|
state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
|
||||||
|
ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](cm);
|
||||||
|
return;
|
||||||
|
} else if (!reverse && searchAppliesTo(cm)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
|
||||||
|
if (reverse && searchAppliesTo(cm)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// nothing found so far, so call the original search with wrap-around
|
||||||
|
ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](activeCM);
|
||||||
|
|
||||||
|
function searchAppliesTo(cm) {
|
||||||
|
let inputs = $$('.applies-value', cm.getSection());
|
||||||
|
if (reverse) {
|
||||||
|
inputs = inputs.reverse();
|
||||||
|
}
|
||||||
|
inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
|
||||||
|
return inputs.some(input => {
|
||||||
|
const match = rxQuery.exec(input.value);
|
||||||
|
if (match) {
|
||||||
|
input.focus();
|
||||||
|
const end = match.index + match[0].length;
|
||||||
|
// scroll selected part into view in long inputs,
|
||||||
|
// works only outside of current event handlers chain, hence timeout=0
|
||||||
|
setTimeout(() => {
|
||||||
|
input.setSelectionRange(end, end);
|
||||||
|
input.setSelectionRange(match.index, end);
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPrev(cm) {
|
||||||
|
findNext(cm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(activeCM, all) {
|
||||||
|
let queue;
|
||||||
|
let query;
|
||||||
|
let replacement;
|
||||||
|
activeCM = focusClosestCM(activeCM);
|
||||||
|
customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) {
|
||||||
|
query = txt;
|
||||||
|
customizeOpenDialog(activeCM, template.replaceWith, txt => {
|
||||||
|
replacement = txt;
|
||||||
|
queue = editors.rotate(-editors.indexOf(activeCM));
|
||||||
|
if (all) {
|
||||||
|
editors.forEach(doReplace);
|
||||||
|
} else {
|
||||||
|
doReplace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this(query);
|
||||||
|
});
|
||||||
|
ORIGINAL_COMMAND.replace(activeCM, all);
|
||||||
|
|
||||||
|
function doReplace() {
|
||||||
|
const cm = queue.shift();
|
||||||
|
if (!cm) {
|
||||||
|
if (!all) {
|
||||||
|
editors.lastActive.focus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// hide the first two dialogs (replace, replaceWith)
|
||||||
|
cm.openDialog = (tmpl, callback) => {
|
||||||
|
cm.openDialog = (tmpl, callback) => {
|
||||||
|
cm.openDialog = ORIGINAL_METHOD.openDialog;
|
||||||
|
if (all) {
|
||||||
|
callback(replacement);
|
||||||
|
} else {
|
||||||
|
doConfirm(cm);
|
||||||
|
callback(replacement);
|
||||||
|
if (!$('.CodeMirror-dialog', cm.getWrapperElement())) {
|
||||||
|
// no dialog == nothing found in the current CM, move to the next
|
||||||
|
doReplace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
callback(query);
|
||||||
|
};
|
||||||
|
ORIGINAL_COMMAND.replace(cm, all);
|
||||||
|
}
|
||||||
|
function doConfirm(cm) {
|
||||||
|
let wrapAround = false;
|
||||||
|
const origPos = cm.getCursor();
|
||||||
|
cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
|
||||||
|
const ovrCallbacks = callbacks.map(callback => () => {
|
||||||
|
makeSectionVisible(cm);
|
||||||
|
cm.openConfirm = overrideConfirm;
|
||||||
|
setTimeout(() => (cm.openConfirm = ORIGINAL_METHOD.openConfirm));
|
||||||
|
|
||||||
|
const pos = cm.getCursor();
|
||||||
|
callback();
|
||||||
|
const cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
|
||||||
|
wrapAround |= cmp <= 0;
|
||||||
|
|
||||||
|
const dlg = $('.CodeMirror-dialog', cm.getWrapperElement());
|
||||||
|
if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
|
||||||
|
$.remove(dlg);
|
||||||
|
doReplace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ORIGINAL_METHOD.openConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceAll(cm) {
|
||||||
|
replace(cm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function rerouteHotkeys(enable, immediately) {
|
||||||
|
if (!immediately) {
|
||||||
|
debounce(rerouteHotkeys, 0, enable, true);
|
||||||
|
} else if (enable) {
|
||||||
|
document.addEventListener('keydown', rerouteHandler);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', rerouteHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerouteHandler(event) {
|
||||||
|
const keyName = CodeMirror.keyName(event);
|
||||||
|
if (!keyName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rerouteCommand = name => {
|
||||||
|
if (REROUTED.has(name)) {
|
||||||
|
CodeMirror.commands[name](getEditorInSight(event.target));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
||||||
|
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function getEditorInSight(nearbyElement) {
|
||||||
|
// priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
|
||||||
|
let cm;
|
||||||
|
if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) {
|
||||||
|
cm = getSectionForChild(nearbyElement).CodeMirror;
|
||||||
|
} else {
|
||||||
|
cm = editors.lastActive;
|
||||||
|
}
|
||||||
|
// closest editor should have at least 2 lines visible
|
||||||
|
const lineHeight = editors[0].defaultTextHeight();
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
||||||
|
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
||||||
|
const distances = [];
|
||||||
|
const alreadyInView = cm && offscreenDistance(null, cm) === 0;
|
||||||
|
return alreadyInView ? cm : findClosest();
|
||||||
|
|
||||||
|
function offscreenDistance(index, cm) {
|
||||||
|
if (index >= 0 && distances[index] !== undefined) {
|
||||||
|
return distances[index];
|
||||||
|
}
|
||||||
|
const section = (cm || editors[index]).getSection();
|
||||||
|
const top = allSectionsContainerTop + section.offsetTop;
|
||||||
|
if (top < scrollY + lineHeight) {
|
||||||
|
return Math.max(0, scrollY - top - lineHeight);
|
||||||
|
}
|
||||||
|
if (top < windowBottom) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const distance = top - windowBottom + section.offsetHeight;
|
||||||
|
if (index >= 0) {
|
||||||
|
distances[index] = distance;
|
||||||
|
}
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosest() {
|
||||||
|
const last = editors.length - 1;
|
||||||
|
let a = 0;
|
||||||
|
let b = last;
|
||||||
|
let c;
|
||||||
|
let distance;
|
||||||
|
while (a < b - 1) {
|
||||||
|
c = (a + b) / 2 | 0;
|
||||||
|
distance = offscreenDistance(c);
|
||||||
|
if (!distance || !c) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const distancePrev = offscreenDistance(c - 1);
|
||||||
|
const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
|
||||||
|
if (distancePrev <= distance && distance <= distanceNext) {
|
||||||
|
b = c;
|
||||||
|
} else {
|
||||||
|
a = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
|
||||||
|
b--;
|
||||||
|
}
|
||||||
|
const cm = editors[b];
|
||||||
|
if (distances[b] > 0) {
|
||||||
|
makeSectionVisible(cm);
|
||||||
|
}
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function getCodeMirrorThemes() {
|
||||||
|
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||||
|
const themes = [
|
||||||
|
chrome.i18n.getMessage('defaultTheme'),
|
||||||
|
'3024-day',
|
||||||
|
'3024-night',
|
||||||
|
'abcdef',
|
||||||
|
'ambiance',
|
||||||
|
'ambiance-mobile',
|
||||||
|
'base16-dark',
|
||||||
|
'base16-light',
|
||||||
|
'bespin',
|
||||||
|
'blackboard',
|
||||||
|
'cobalt',
|
||||||
|
'colorforth',
|
||||||
|
'dracula',
|
||||||
|
'duotone-dark',
|
||||||
|
'duotone-light',
|
||||||
|
'eclipse',
|
||||||
|
'elegant',
|
||||||
|
'erlang-dark',
|
||||||
|
'hopscotch',
|
||||||
|
'icecoder',
|
||||||
|
'isotope',
|
||||||
|
'lesser-dark',
|
||||||
|
'liquibyte',
|
||||||
|
'material',
|
||||||
|
'mbo',
|
||||||
|
'mdn-like',
|
||||||
|
'midnight',
|
||||||
|
'monokai',
|
||||||
|
'neat',
|
||||||
|
'neo',
|
||||||
|
'night',
|
||||||
|
'panda-syntax',
|
||||||
|
'paraiso-dark',
|
||||||
|
'paraiso-light',
|
||||||
|
'pastel-on-dark',
|
||||||
|
'railscasts',
|
||||||
|
'rubyblue',
|
||||||
|
'seti',
|
||||||
|
'solarized',
|
||||||
|
'the-matrix',
|
||||||
|
'tomorrow-night-bright',
|
||||||
|
'tomorrow-night-eighties',
|
||||||
|
'ttcn',
|
||||||
|
'twilight',
|
||||||
|
'vibrant-ink',
|
||||||
|
'xq-dark',
|
||||||
|
'xq-light',
|
||||||
|
'yeti',
|
||||||
|
'zenburn',
|
||||||
|
];
|
||||||
|
localStorage.codeMirrorThemes = themes.join(' ');
|
||||||
|
return Promise.resolve(themes);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||||
|
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
||||||
|
themeDir.createReader().readEntries(entries => {
|
||||||
|
const themes = [
|
||||||
|
chrome.i18n.getMessage('defaultTheme')
|
||||||
|
].concat(
|
||||||
|
entries.filter(entry => entry.isFile)
|
||||||
|
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||||
|
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||||
|
);
|
||||||
|
localStorage.codeMirrorThemes = themes.join(' ');
|
||||||
|
resolve(themes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showKeyInSaveButtonTooltip(prefName, value) {
|
||||||
|
$('#save-button').title = findKeyForCommand('save', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
|
||||||
|
const map = CodeMirror.keyMap[mapName];
|
||||||
|
let key = Object.keys(map).find(k => map[k] === command);
|
||||||
|
if (key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
||||||
|
key = ft && findKeyForCommand(command, ft);
|
||||||
|
if (key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,313 +0,0 @@
|
||||||
/* global CodeMirror */
|
|
||||||
/* global editor */
|
|
||||||
/* global prefs */
|
|
||||||
/* global rerouteHotkeys */// util.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
All cm instances created by this module are collected so we can broadcast prefs
|
|
||||||
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
|
||||||
when the instance is not used anymore.
|
|
||||||
*/
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
//#region Factory
|
|
||||||
|
|
||||||
const cms = new Set();
|
|
||||||
let lazyOpt;
|
|
||||||
|
|
||||||
const cmFactory = window.cmFactory = {
|
|
||||||
|
|
||||||
create(place, options) {
|
|
||||||
const cm = CodeMirror(place, options);
|
|
||||||
cm.lastActive = 0;
|
|
||||||
cms.add(cm);
|
|
||||||
return cm;
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy(cm) {
|
|
||||||
cms.delete(cm);
|
|
||||||
},
|
|
||||||
|
|
||||||
globalSetOption(key, value) {
|
|
||||||
CodeMirror.defaults[key] = value;
|
|
||||||
if (cms.size > 4 && lazyOpt.names.includes(key)) {
|
|
||||||
lazyOpt.set(key, value);
|
|
||||||
} else {
|
|
||||||
cms.forEach(cm => cm.setOption(key, value));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// focus and blur
|
|
||||||
|
|
||||||
const onCmFocus = cm => {
|
|
||||||
rerouteHotkeys.toggle(false);
|
|
||||||
cm.display.wrapper.classList.add('CodeMirror-active');
|
|
||||||
cm.lastActive = Date.now();
|
|
||||||
};
|
|
||||||
const onCmBlur = cm => {
|
|
||||||
setTimeout(() => {
|
|
||||||
/* Delaying to next tick to avoid double-processing of the currently processed keyboard event
|
|
||||||
* when it bubbles up from CodeMirror to `document` where the rerouter listens */
|
|
||||||
rerouteHotkeys.toggle(true);
|
|
||||||
const {wrapper} = cm.display;
|
|
||||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
CodeMirror.defineInitHook(cm => {
|
|
||||||
cm.on('focus', onCmFocus);
|
|
||||||
cm.on('blur', onCmBlur);
|
|
||||||
});
|
|
||||||
|
|
||||||
// propagated preferences
|
|
||||||
|
|
||||||
const prefToCmOpt = k =>
|
|
||||||
k.startsWith('editor.') &&
|
|
||||||
k.slice('editor.'.length);
|
|
||||||
const prefKeys = prefs.knownKeys.filter(k =>
|
|
||||||
k !== 'editor.colorpicker' && // handled in colorpicker-helper.js
|
|
||||||
k !== 'editor.arrowKeysTraverse' && // handled in sections-editor.js
|
|
||||||
prefToCmOpt(k) in CodeMirror.defaults);
|
|
||||||
const {insertTab, insertSoftTab} = CodeMirror.commands;
|
|
||||||
|
|
||||||
for (const [key, fn] of Object.entries({
|
|
||||||
'editor.tabSize'(cm, value) {
|
|
||||||
cm.setOption('indentUnit', Number(value));
|
|
||||||
},
|
|
||||||
'editor.indentWithTabs'(cm, value) {
|
|
||||||
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
|
|
||||||
},
|
|
||||||
'editor.matchHighlight'(cm, value) {
|
|
||||||
const showToken = value === 'token' && /[#.\-\w]/;
|
|
||||||
const opt = (showToken || value === 'selection') && {
|
|
||||||
showToken,
|
|
||||||
annotateScrollbar: true,
|
|
||||||
delay: 0,
|
|
||||||
onUpdate: updateMatchHighlightCount,
|
|
||||||
};
|
|
||||||
cm.setOption('highlightSelectionMatches', opt || null);
|
|
||||||
},
|
|
||||||
'editor.selectByTokens'(cm, value) {
|
|
||||||
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
|
||||||
},
|
|
||||||
})) {
|
|
||||||
CodeMirror.defineOption(prefToCmOpt(key), prefs.get(key), fn);
|
|
||||||
prefKeys.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.subscribe(prefKeys, (key, val) => {
|
|
||||||
if (key === 'editor.theme') editor.updateTheme(val);
|
|
||||||
cmFactory.globalSetOption(prefToCmOpt(key), val);
|
|
||||||
});
|
|
||||||
|
|
||||||
// lazy propagation
|
|
||||||
|
|
||||||
lazyOpt = {
|
|
||||||
names: ['theme', 'lineWrapping'],
|
|
||||||
set(key, value) {
|
|
||||||
const {observer, queue} = lazyOpt;
|
|
||||||
for (const cm of cms) {
|
|
||||||
let opts = queue.get(cm);
|
|
||||||
if (!opts) queue.set(cm, opts = {});
|
|
||||||
opts[key] = value;
|
|
||||||
observer.observe(cm.display.wrapper);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setNow({cm, data}) {
|
|
||||||
cm.operation(() => data.forEach(kv => cm.setOption(...kv)));
|
|
||||||
},
|
|
||||||
onView(entries) {
|
|
||||||
const {queue, observer} = lazyOpt;
|
|
||||||
const delayed = [];
|
|
||||||
for (const e of entries) {
|
|
||||||
const r = e.isIntersecting && e.intersectionRect;
|
|
||||||
if (!r) continue;
|
|
||||||
const cm = e.target.CodeMirror;
|
|
||||||
const data = Object.entries(queue.get(cm) || {});
|
|
||||||
queue.delete(cm);
|
|
||||||
observer.unobserve(e.target);
|
|
||||||
if (!data.every(([key, val]) => cm.getOption(key) === val)) {
|
|
||||||
if (r.bottom > 0 && r.top < window.innerHeight) {
|
|
||||||
lazyOpt.setNow({cm, data});
|
|
||||||
} else {
|
|
||||||
delayed.push({cm, data});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (delayed.length) {
|
|
||||||
setTimeout(() => delayed.forEach(lazyOpt.setNow));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get observer() {
|
|
||||||
if (!lazyOpt._observer) {
|
|
||||||
// must exceed refreshOnView's 100%
|
|
||||||
lazyOpt._observer = new IntersectionObserver(lazyOpt.onView, {rootMargin: '150%'});
|
|
||||||
lazyOpt.queue = new WeakMap();
|
|
||||||
}
|
|
||||||
return lazyOpt._observer;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Commands
|
|
||||||
|
|
||||||
Object.assign(CodeMirror.commands, {
|
|
||||||
commentSelection(cm) {
|
|
||||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
|
||||||
},
|
|
||||||
toggleEditorFocus(cm) {
|
|
||||||
if (!cm) return;
|
|
||||||
if (cm.hasFocus()) {
|
|
||||||
setTimeout(() => cm.display.input.blur());
|
|
||||||
} else {
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
for (const cmd of [
|
|
||||||
'nextEditor',
|
|
||||||
'prevEditor',
|
|
||||||
'save',
|
|
||||||
'toggleStyle',
|
|
||||||
]) {
|
|
||||||
CodeMirror.commands[cmd] = (...args) => editor[cmd](...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region CM option handlers
|
|
||||||
|
|
||||||
function updateMatchHighlightCount(cm, state) {
|
|
||||||
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configureMouseFn(cm, repeat) {
|
|
||||||
return repeat === 'double' ?
|
|
||||||
{unit: selectTokenOnDoubleclick} :
|
|
||||||
{};
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTokenOnDoubleclick(cm, pos) {
|
|
||||||
let {ch} = pos;
|
|
||||||
const {line, sticky} = pos;
|
|
||||||
const {text, styles} = cm.getLineHandle(line);
|
|
||||||
|
|
||||||
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
|
||||||
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
|
||||||
const atWord = ch => at(/\w/y, ch);
|
|
||||||
const atSpace = ch => at(/\s/y, ch);
|
|
||||||
|
|
||||||
const atTokenEnd = styles.indexOf(ch, 1);
|
|
||||||
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
|
||||||
ch = Math.min(text.length, ch);
|
|
||||||
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
|
||||||
if (atTokenEnd > 0) ch--;
|
|
||||||
|
|
||||||
const isCss = type && !/^(comment|string)/.test(type);
|
|
||||||
const isNumber = type === 'number';
|
|
||||||
const isSpace = atSpace(ch);
|
|
||||||
let wordChars =
|
|
||||||
isNumber ? /[-+\w.%]/y :
|
|
||||||
isCss ? /[-\w@]/y :
|
|
||||||
isSpace ? /\s/y :
|
|
||||||
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
|
||||||
|
|
||||||
let a = ch;
|
|
||||||
while (a && at(wordChars, a)) a--;
|
|
||||||
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
|
||||||
|
|
||||||
let b, found;
|
|
||||||
|
|
||||||
if (isNumber) {
|
|
||||||
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
|
||||||
found = b >= ch;
|
|
||||||
if (!found) {
|
|
||||||
a = b;
|
|
||||||
ch = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
|
||||||
b = ch + execAt(wordChars, ch)[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: {line, ch: a},
|
|
||||||
to: {line, ch: b},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region Bookmarks
|
|
||||||
|
|
||||||
const BM_CLS = 'gutter-bookmark';
|
|
||||||
const BM_BRAND = 'sublimeBookmark';
|
|
||||||
const BM_CLICKER = 'CodeMirror-linenumbers';
|
|
||||||
const BM_DATA = Symbol('data');
|
|
||||||
// TODO: revisit when https://github.com/codemirror/CodeMirror/issues/6716 is fixed
|
|
||||||
const tmProto = CodeMirror.TextMarker.prototype;
|
|
||||||
const tmProtoOvr = {};
|
|
||||||
for (const k of ['clear', 'attachLine', 'detachLine']) {
|
|
||||||
tmProtoOvr[k] = function (line) {
|
|
||||||
const {cm} = this.doc;
|
|
||||||
const withOp = !cm.curOp;
|
|
||||||
if (withOp) cm.startOperation();
|
|
||||||
tmProto[k].apply(this, arguments);
|
|
||||||
cm.curOp.ownsGroup.delayedCallbacks.push(toggleMark.bind(this, this.lines[0], line));
|
|
||||||
if (withOp) cm.endOperation();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
for (const name of ['prevBookmark', 'nextBookmark']) {
|
|
||||||
const cmdFn = CodeMirror.commands[name];
|
|
||||||
CodeMirror.commands[name] = cm => {
|
|
||||||
cm.setSelection = cm.jumpToPos;
|
|
||||||
cmdFn(cm);
|
|
||||||
delete cm.setSelection;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
CodeMirror.defineInitHook(cm => {
|
|
||||||
cm.on('gutterClick', onGutterClick);
|
|
||||||
cm.on('gutterContextMenu', onGutterContextMenu);
|
|
||||||
cm.on('markerAdded', onMarkAdded);
|
|
||||||
});
|
|
||||||
// TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
|
|
||||||
function onGutterClick(cm, line, name, e) {
|
|
||||||
switch (name === BM_CLICKER && e.button) {
|
|
||||||
case 0: {
|
|
||||||
// main button: toggle
|
|
||||||
const [mark] = cm.findMarks({line, ch: 0}, {line, ch: 1e9}, m => m[BM_BRAND]);
|
|
||||||
cm.setCursor(mark ? mark.find(-1) : {line, ch: 0});
|
|
||||||
cm.execCommand('toggleBookmark');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
// middle button: select all marks
|
|
||||||
cm.execCommand('selectBookmarks');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onGutterContextMenu(cm, line, name, e) {
|
|
||||||
if (name === BM_CLICKER) {
|
|
||||||
cm.execCommand(e.ctrlKey ? 'prevBookmark' : 'nextBookmark');
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onMarkAdded(cm, mark) {
|
|
||||||
if (mark[BM_BRAND]) {
|
|
||||||
// CM bug workaround to keep the mark at line start when the above line is removed
|
|
||||||
mark.inclusiveRight = true;
|
|
||||||
Object.assign(mark, tmProtoOvr);
|
|
||||||
toggleMark.call(mark, true, mark[BM_DATA] = mark.lines[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function toggleMark(state, line = this[BM_DATA]) {
|
|
||||||
this.doc[state ? 'addLineClass' : 'removeLineClass'](line, 'gutter', BM_CLS);
|
|
||||||
if (state) {
|
|
||||||
const bms = this.doc.cm.state.sublimeBookmarks;
|
|
||||||
if (!bms.includes(this)) bms.push(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
})();
|
|
File diff suppressed because one or more lines are too long
159
edit/colorpicker-helper.js
Normal file
159
edit/colorpicker-helper.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/* global CodeMirror loadScript editors showHelp */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
onDOMscriptReady('/colorview.js').then(() => {
|
||||||
|
initOverlayHooks();
|
||||||
|
onDOMready().then(() => {
|
||||||
|
$('#colorpicker-settings').onclick = configureColorpicker;
|
||||||
|
});
|
||||||
|
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
|
||||||
|
prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
|
||||||
|
setColorpickerOption(null, prefs.get('editor.colorpicker'));
|
||||||
|
|
||||||
|
function setColorpickerOption(id, enabled) {
|
||||||
|
const defaults = CodeMirror.defaults;
|
||||||
|
const keyName = prefs.get('editor.colorpicker.hotkey');
|
||||||
|
defaults.colorpicker = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
if (keyName) {
|
||||||
|
CodeMirror.commands.colorpicker = invokeColorpicker;
|
||||||
|
defaults.extraKeys = defaults.extraKeys || {};
|
||||||
|
defaults.extraKeys[keyName] = 'colorpicker';
|
||||||
|
}
|
||||||
|
defaults.colorpicker = {
|
||||||
|
forceUpdate: editors.length > 0,
|
||||||
|
tooltip: t('colorpickerTooltip'),
|
||||||
|
popupOptions: {
|
||||||
|
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||||
|
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
|
||||||
|
hideDelay: 5000,
|
||||||
|
embedderCallback: state => {
|
||||||
|
['hexUppercase', 'color']
|
||||||
|
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
|
||||||
|
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
CodeMirror.modeExtensions.css.unregisterColorviewHooks();
|
||||||
|
if (defaults.extraKeys) {
|
||||||
|
delete defaults.extraKeys[keyName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// on page load runs before CodeMirror.setOption is defined
|
||||||
|
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerHotkey(id, hotkey) {
|
||||||
|
CodeMirror.commands.colorpicker = invokeColorpicker;
|
||||||
|
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||||
|
for (const key in extraKeys) {
|
||||||
|
if (extraKeys[key] === 'colorpicker') {
|
||||||
|
delete extraKeys[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hotkey) {
|
||||||
|
extraKeys[hotkey] = 'colorpicker';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeColorpicker(cm) {
|
||||||
|
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureColorpicker() {
|
||||||
|
const input = $create('input', {
|
||||||
|
type: 'search',
|
||||||
|
spellcheck: false,
|
||||||
|
value: prefs.get('editor.colorpicker.hotkey'),
|
||||||
|
onkeydown(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const key = CodeMirror.keyName(event);
|
||||||
|
switch (key) {
|
||||||
|
case 'Enter':
|
||||||
|
if (this.checkValidity()) {
|
||||||
|
$('#help-popup .dismiss').onclick();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'Esc':
|
||||||
|
$('#help-popup .dismiss').onclick();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
// disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys
|
||||||
|
if (!key || new RegExp('^(' + [
|
||||||
|
'(Back)?Space',
|
||||||
|
'(Shift-)?.', // a single character
|
||||||
|
'(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)',
|
||||||
|
].join('|') + ')$', 'i').test(key)) {
|
||||||
|
this.value = key || this.value;
|
||||||
|
this.setCustomValidity('Not allowed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = key;
|
||||||
|
this.setCustomValidity('');
|
||||||
|
prefs.set('editor.colorpicker.hotkey', key);
|
||||||
|
},
|
||||||
|
oninput() {
|
||||||
|
// fired on pressing "x" to clear the field
|
||||||
|
prefs.set('editor.colorpicker.hotkey', '');
|
||||||
|
},
|
||||||
|
onpaste(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const popup = showHelp(t('helpKeyMapHotkey'), input);
|
||||||
|
if (this instanceof Element) {
|
||||||
|
const bounds = this.getBoundingClientRect();
|
||||||
|
popup.style.left = bounds.right + 10 + 'px';
|
||||||
|
popup.style.top = bounds.top - popup.clientHeight / 2 + 'px';
|
||||||
|
popup.style.right = 'auto';
|
||||||
|
}
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initOverlayHooks() {
|
||||||
|
const COLORVIEW_DISABLED_SUFFIX = ' colorview-disabled';
|
||||||
|
const COLORVIEW_NEXT_DISABLED_SUFFIX = ' colorview-next-disabled';
|
||||||
|
const originalAddOverlay = CodeMirror.prototype.addOverlay;
|
||||||
|
CodeMirror.prototype.addOverlay = addOverlayHook;
|
||||||
|
|
||||||
|
function addOverlayHook(overlay) {
|
||||||
|
if (overlay.token !== tokenHook && (
|
||||||
|
overlay === (this.state.matchHighlighter || {}).overlay ||
|
||||||
|
overlay === (this.state.search || {}).overlay)) {
|
||||||
|
overlay.colopickerHelper = {token: overlay.token};
|
||||||
|
overlay.token = tokenHook;
|
||||||
|
}
|
||||||
|
originalAddOverlay.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenHook(stream) {
|
||||||
|
const style = this.colopickerHelper.token.apply(this, arguments);
|
||||||
|
if (!style) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
const {start, pos, lineOracle: {baseTokens}} = stream;
|
||||||
|
if (!baseTokens) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
for (let prev = 0, i = 1; i < baseTokens.length; i += 2) {
|
||||||
|
const end = baseTokens[i];
|
||||||
|
if (prev <= start && start <= end) {
|
||||||
|
const base = baseTokens[i + 1];
|
||||||
|
if (base && base.includes('colorview')) {
|
||||||
|
return style +
|
||||||
|
(start > prev ? COLORVIEW_DISABLED_SUFFIX : '') +
|
||||||
|
(pos < end ? COLORVIEW_NEXT_DISABLED_SUFFIX : '');
|
||||||
|
}
|
||||||
|
} else if (end > pos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prev = end;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,68 +0,0 @@
|
||||||
/* global messageBoxProxy */// dom.js
|
|
||||||
/* global API */// msg.js
|
|
||||||
/* global clamp debounce */// toolbox.js
|
|
||||||
/* global editor */
|
|
||||||
/* global prefs */
|
|
||||||
/* global t */// localization.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(async function AutosaveDrafts() {
|
|
||||||
const makeId = () => editor.style.id || 'new';
|
|
||||||
let delay;
|
|
||||||
let port;
|
|
||||||
connectPort();
|
|
||||||
|
|
||||||
const draft = await API.drafts.get(makeId());
|
|
||||||
if (draft && draft.isUsercss === editor.isUsercss) {
|
|
||||||
const date = makeRelativeDate(draft.date);
|
|
||||||
if (await messageBoxProxy.confirm(t('draftAction'), 'danger', t('draftTitle', date))) {
|
|
||||||
await editor.replaceStyle(draft.style, draft);
|
|
||||||
} else {
|
|
||||||
API.drafts.delete(makeId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.dirty.onChange(isDirty => isDirty ? connectPort() : port.disconnect());
|
|
||||||
editor.dirty.onDataChange(isDirty => debounce(updateDraft, isDirty ? delay : 0));
|
|
||||||
|
|
||||||
prefs.subscribe('editor.autosaveDraft', (key, val) => {
|
|
||||||
delay = clamp(val * 1000 | 0, 1000, 2 ** 32 - 1);
|
|
||||||
const t = debounce.timers.get(updateDraft);
|
|
||||||
if (t != null) debounce(updateDraft, t ? delay : 0);
|
|
||||||
}, {runNow: true});
|
|
||||||
|
|
||||||
function connectPort() {
|
|
||||||
port = chrome.runtime.connect({name: 'draft:' + makeId()});
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeRelativeDate(date) {
|
|
||||||
let delta = (Date.now() - date) / 1000;
|
|
||||||
if (delta >= 0 && Intl.RelativeTimeFormat) {
|
|
||||||
for (const [span, unit, frac = 1] of [
|
|
||||||
[60, 'second', 0],
|
|
||||||
[60, 'minute', 0],
|
|
||||||
[24, 'hour'],
|
|
||||||
[7, 'day'],
|
|
||||||
[4, 'week'],
|
|
||||||
[12, 'month'],
|
|
||||||
[1e99, 'year'],
|
|
||||||
]) {
|
|
||||||
if (delta < span) {
|
|
||||||
return new Intl.RelativeTimeFormat({style: 'short'}).format(-delta.toFixed(frac), unit);
|
|
||||||
}
|
|
||||||
delta /= span;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return date.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDraft(isDirty = editor.dirty.isDirty()) {
|
|
||||||
if (!isDirty) return;
|
|
||||||
API.drafts.put({
|
|
||||||
date: Date.now(),
|
|
||||||
isUsercss: editor.isUsercss,
|
|
||||||
style: editor.getValue(true),
|
|
||||||
si: editor.makeScrollInfo(),
|
|
||||||
}, makeId());
|
|
||||||
}
|
|
||||||
})();
|
|
985
edit/edit.css
985
edit/edit.css
File diff suppressed because it is too large
Load Diff
980
edit/edit.js
980
edit/edit.js
File diff suppressed because it is too large
Load Diff
|
@ -1,176 +0,0 @@
|
||||||
/* global createWorkerApi */// worker-util.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
let sugarss = false;
|
|
||||||
|
|
||||||
/** @namespace EditorWorker */
|
|
||||||
createWorkerApi({
|
|
||||||
|
|
||||||
async csslint(code, config) {
|
|
||||||
require(['/js/csslint/parserlib', '/js/csslint/csslint']); /* global CSSLint */
|
|
||||||
return CSSLint
|
|
||||||
.verify(code, config).messages
|
|
||||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
|
||||||
},
|
|
||||||
|
|
||||||
getCssPropsValues() {
|
|
||||||
require(['/js/csslint/parserlib']); /* global parserlib */
|
|
||||||
const {
|
|
||||||
css: {Colors, GlobalKeywords, Properties},
|
|
||||||
util: {describeProp},
|
|
||||||
} = parserlib;
|
|
||||||
const namedColors = Object.keys(Colors);
|
|
||||||
const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g;
|
|
||||||
const res = {};
|
|
||||||
// moving vendor-prefixed props to the end
|
|
||||||
const cmp = (a, b) => a[0] === '-' && b[0] !== '-' ? 1 : a < b ? -1 : a > b;
|
|
||||||
for (const [k, v] of Object.entries(Properties)) {
|
|
||||||
res[k] = false;
|
|
||||||
if (typeof v === 'string') {
|
|
||||||
let last = '';
|
|
||||||
const uniq = [];
|
|
||||||
// strip definitions of function arguments
|
|
||||||
const desc = describeProp(v).replace(/([-\w]+)\(.*?\)/g, 'z-$1');
|
|
||||||
const descNoColors = desc.replace(/<named-color>/g, '');
|
|
||||||
// add a prefix to functions to group them at the end
|
|
||||||
const words = descNoColors.split(rxNonWord).sort(cmp);
|
|
||||||
for (let w of words) {
|
|
||||||
if (w.startsWith('z-')) w = w.slice(2) + '(';
|
|
||||||
if (w !== last) uniq.push(last = w);
|
|
||||||
}
|
|
||||||
if (desc !== descNoColors) uniq.push(...namedColors);
|
|
||||||
if (uniq.length) res[k] = uniq;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {all: res, global: GlobalKeywords};
|
|
||||||
},
|
|
||||||
|
|
||||||
getRules(linter) {
|
|
||||||
return ruleRetriever[linter](); // eslint-disable-line no-use-before-define
|
|
||||||
},
|
|
||||||
|
|
||||||
metalint(code) {
|
|
||||||
require(['/js/meta-parser']); /* global metaParser */
|
|
||||||
const result = metaParser.lint(code);
|
|
||||||
// extract needed info
|
|
||||||
result.errors = result.errors.map(err => ({
|
|
||||||
code: err.code,
|
|
||||||
args: err.args,
|
|
||||||
message: err.message,
|
|
||||||
index: err.index,
|
|
||||||
}));
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
async stylelint(opts) {
|
|
||||||
require(['/vendor/stylelint-bundle/stylelint-bundle.min']); /* global stylelint */
|
|
||||||
// Stylus-lang allows a trailing ";" but sugarss doesn't, so we monkeypatch it
|
|
||||||
stylelint.SugarSSParser.prototype.checkSemicolon = tt => {
|
|
||||||
while (tt.length && tt[tt.length - 1][0] === ';') tt.pop();
|
|
||||||
};
|
|
||||||
for (const pass of opts.mode === 'stylus' ? [sugarss, !sugarss] : [-1]) {
|
|
||||||
/* We try sugarss (for indented stylus-lang), then css mode, switching them on failure,
|
|
||||||
* so that the succeeding syntax will be used next time first. */
|
|
||||||
opts.config.customSyntax = !pass ? 'sugarss' : '';
|
|
||||||
try {
|
|
||||||
const res = await stylelint.createLinter(opts)._lintSource(opts);
|
|
||||||
if (pass !== -1) sugarss = pass;
|
|
||||||
return collectStylelintResults(res, opts);
|
|
||||||
} catch (e) {
|
|
||||||
const fatal = pass === -1 ||
|
|
||||||
!pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e) ||
|
|
||||||
pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(`${e}${e.stack}`);
|
|
||||||
if (fatal) {
|
|
||||||
return [{
|
|
||||||
from: {line: e.line - 1, ch: e.column - 1},
|
|
||||||
to: {line: e.line - 1, ch: e.column - 1},
|
|
||||||
message: e.reason,
|
|
||||||
severity: 'error',
|
|
||||||
rule: e.name,
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ruleRetriever = {
|
|
||||||
|
|
||||||
csslint() {
|
|
||||||
require(['/js/csslint/csslint']);
|
|
||||||
return CSSLint.getRuleList().map(rule => {
|
|
||||||
const output = {};
|
|
||||||
for (const [key, value] of Object.entries(rule)) {
|
|
||||||
if (typeof value !== 'function') {
|
|
||||||
output[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
stylelint() {
|
|
||||||
require(['/vendor/stylelint-bundle/stylelint-bundle.min']);
|
|
||||||
const options = {};
|
|
||||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
|
||||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
|
||||||
for (const [id, rule] of Object.entries(stylelint.rules)) {
|
|
||||||
const ruleCode = `${rule()}`;
|
|
||||||
const sets = [];
|
|
||||||
let m, mStr;
|
|
||||||
while ((m = rxPossible.exec(ruleCode))) {
|
|
||||||
const possible = m[1];
|
|
||||||
const set = [];
|
|
||||||
while ((mStr = rxString.exec(possible))) {
|
|
||||||
const s = mStr[1];
|
|
||||||
if (s.includes(' ')) {
|
|
||||||
set.push(...s.split(/\s+/));
|
|
||||||
} else {
|
|
||||||
set.push(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (possible.includes('ignoreAtRules')) {
|
|
||||||
set.push('ignoreAtRules');
|
|
||||||
}
|
|
||||||
if (possible.includes('ignoreShorthands')) {
|
|
||||||
set.push('ignoreShorthands');
|
|
||||||
}
|
|
||||||
if (set.length) {
|
|
||||||
sets.push(set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options[id] = sets;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function collectStylelintResults({messages}, {mode}) {
|
|
||||||
/* We hide nonfatal "//" warnings since we lint with sugarss without applying @preprocessor.
|
|
||||||
* We can't easily pre-remove "//" comments which may be inside strings, comments, url(), etc.
|
|
||||||
* And even if we did, it'd be wrong to hide potential bugs in stylus-lang like #1460 */
|
|
||||||
const isLess = mode === 'text/x-less';
|
|
||||||
const slashCommentAllowed = isLess || mode === 'stylus';
|
|
||||||
const res = [];
|
|
||||||
for (const m of messages) {
|
|
||||||
if (/deprecation|invalidOption/.test(m.stylelintType)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const {rule} = m;
|
|
||||||
const msg = m.text.replace(/^Unexpected\s+/, '').replace(` (${rule})`, '');
|
|
||||||
if (slashCommentAllowed && msg.includes('"//"') ||
|
|
||||||
isLess && /^unknown at-rule "@[-\w]+:"/.test(msg) /* LESS variables */) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
res.push({
|
|
||||||
from: {line: m.line - 1, ch: m.column - 1},
|
|
||||||
to: {line: m.endLine - 1, ch: m.endColumn - 1},
|
|
||||||
message: msg[0].toUpperCase() + msg.slice(1),
|
|
||||||
severity: m.severity,
|
|
||||||
rule,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,109 +0,0 @@
|
||||||
/* global $ $create $remove getEventKeyName */// dom.js
|
|
||||||
/* global CodeMirror */
|
|
||||||
/* global prefs */
|
|
||||||
/* global t */// localization.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const ID = 'popup-iframe';
|
|
||||||
const SEL = '#' + ID;
|
|
||||||
const URL = chrome.runtime.getManifest().browser_action.default_popup;
|
|
||||||
const POPUP_HOTKEY = 'Shift-Ctrl-Alt-S';
|
|
||||||
/** @type {HTMLIFrameElement} */
|
|
||||||
let frame;
|
|
||||||
let isLoaded;
|
|
||||||
let scrollbarWidth;
|
|
||||||
|
|
||||||
const btn = $create('img', {
|
|
||||||
id: 'popup-button',
|
|
||||||
title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY,
|
|
||||||
onclick: embedPopup,
|
|
||||||
});
|
|
||||||
$.root.appendChild(btn);
|
|
||||||
$.rootCL.add('popup-window');
|
|
||||||
window.on('domReady', () => {
|
|
||||||
document.body.appendChild(btn);
|
|
||||||
// Adding a dummy command to show in keymap help popup
|
|
||||||
CodeMirror.defaults.extraKeys[POPUP_HOTKEY] = 'openStylusPopup';
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
prefs.subscribe('iconset', (_, val) => {
|
|
||||||
const prefix = `images/icon/${val ? 'light/' : ''}`;
|
|
||||||
btn.srcset = `${prefix}16.png 1x,${prefix}32.png 2x`;
|
|
||||||
}, {runNow: true});
|
|
||||||
|
|
||||||
window.on('keydown', e => {
|
|
||||||
if (getEventKeyName(e) === POPUP_HOTKEY) {
|
|
||||||
embedPopup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function embedPopup() {
|
|
||||||
if ($(SEL)) return;
|
|
||||||
isLoaded = false;
|
|
||||||
scrollbarWidth = 0;
|
|
||||||
frame = $create('iframe', {
|
|
||||||
id: ID,
|
|
||||||
src: URL,
|
|
||||||
height: 600,
|
|
||||||
width: prefs.get('popupWidth'),
|
|
||||||
onload: initFrame,
|
|
||||||
});
|
|
||||||
window.on('mousedown', removePopup);
|
|
||||||
document.body.appendChild(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initFrame() {
|
|
||||||
frame = this;
|
|
||||||
frame.focus();
|
|
||||||
const pw = frame.contentWindow;
|
|
||||||
const body = pw.document.body;
|
|
||||||
pw.on('keydown', removePopupOnEsc);
|
|
||||||
pw.close = removePopup;
|
|
||||||
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
|
|
||||||
$create('div', {style: {height: '1px', marginTop: '-1px'}})
|
|
||||||
));
|
|
||||||
new pw.MutationObserver(onMutation).observe(body, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['style'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMutation() {
|
|
||||||
const body = frame.contentDocument.body;
|
|
||||||
const bs = body.style;
|
|
||||||
const w = parseFloat(bs.minWidth || bs.width) + (scrollbarWidth || 0);
|
|
||||||
const h = parseFloat(bs.minHeight || body.offsetHeight);
|
|
||||||
if (frame.width - w) frame.width = w;
|
|
||||||
if (frame.height - h) frame.height = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onIntersect([e]) {
|
|
||||||
const pw = frame.contentWindow;
|
|
||||||
const el = pw.document.scrollingElement;
|
|
||||||
const h = e.isIntersecting && !pw.scrollY ? el.offsetHeight : el.scrollHeight;
|
|
||||||
const hasSB = h > el.offsetHeight;
|
|
||||||
const {width} = e.boundingClientRect;
|
|
||||||
frame.height = h;
|
|
||||||
if (!hasSB !== !scrollbarWidth || frame.width - width) {
|
|
||||||
scrollbarWidth = hasSB ? width - el.offsetWidth : 0;
|
|
||||||
frame.width = width + scrollbarWidth;
|
|
||||||
}
|
|
||||||
if (!isLoaded) {
|
|
||||||
isLoaded = true;
|
|
||||||
frame.dataset.loaded = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePopup() {
|
|
||||||
frame = null;
|
|
||||||
$remove(SEL);
|
|
||||||
window.off('mousedown', removePopup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePopupOnEsc(e) {
|
|
||||||
if (getEventKeyName(e) === 'Escape') {
|
|
||||||
removePopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,247 +0,0 @@
|
||||||
#search-replace-dialog {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 64px;
|
|
||||||
min-width: 200px;
|
|
||||||
max-width: calc(100vw - 4rem);
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 10000;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 4px 5px 20px -6px rgba(0, 0, 0, .5);
|
|
||||||
border: 1px solid hsla(0, 0%, 50%, .4);
|
|
||||||
transition: opacity .25s;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog:not(:focus-within):not(:hover) {
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog > * {
|
|
||||||
padding-right: .75em;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
background-color: hsla(0, 0%, 50%, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="content"] {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="content"] > :not(:last-child) {
|
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="content"] button {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog[data-type="replace"] button svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="input-wrapper"] {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog textarea {
|
|
||||||
resize: none;
|
|
||||||
width: min-content;
|
|
||||||
min-width: 10em;
|
|
||||||
max-width: none;
|
|
||||||
min-height: 1.3em;
|
|
||||||
max-height: 10vh;
|
|
||||||
line-height: 1.3em;
|
|
||||||
padding: .25rem .25rem .25rem .5rem;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
background-color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: pre; /* issue #1000 */
|
|
||||||
color: currentColor; /* use the current theme's color instead of UserAgent's CSS */
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog textarea:invalid {
|
|
||||||
box-shadow: none; /* Firefox is weird */
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog :disabled {
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********** actions ****************/
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="actions"] {
|
|
||||||
flex: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: .5em;
|
|
||||||
opacity: .5;
|
|
||||||
transition: opacity .25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog:focus-within [data-type="actions"],
|
|
||||||
#search-replace-dialog [data-type="actions"]:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-action] {
|
|
||||||
display: block;
|
|
||||||
padding: 2px .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="actions"] a:last-child {
|
|
||||||
margin-right: -.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********** case-sensitivity ****************/
|
|
||||||
|
|
||||||
#search-replace-dialog [data-action="case"] {
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
color: currentColor;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 .5em;
|
|
||||||
line-height: 20px;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-action="case"][data-enabled]:after {
|
|
||||||
content: "";
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
position: absolute;
|
|
||||||
border-color: hsla(180, 100%, 30%, .5);
|
|
||||||
border-style: none none solid none;
|
|
||||||
border-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-action="case"]:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********** clear input ****************/
|
|
||||||
|
|
||||||
#search-replace-dialog textarea:not(:valid) + [data-type="hover"] {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="hover"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 4px;
|
|
||||||
bottom: 0;
|
|
||||||
margin: 0;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity .5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog:hover [data-type="hover"] {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-action="clear"] {
|
|
||||||
padding: 3px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: hsla(0, 0%, 100%, .75);
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="status"] {
|
|
||||||
background-color: hsla(0, 0%, 50%, .2);
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-left: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="tally"] {
|
|
||||||
opacity: .5;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-replace-dialog [data-type="tally"]:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********** CM search highlight restyling, which shouldn't need color variables ****************/
|
|
||||||
body.find-open .search-target-editor {
|
|
||||||
box-shadow: 0 0 0 1px hsl(33, 100%, 50%), 0 0 3px hsla(33, 100%, 50%, .4);
|
|
||||||
/* same as our global.css focus rule */
|
|
||||||
}
|
|
||||||
|
|
||||||
body.find-open .cm-searching {
|
|
||||||
background-color: rgba(255, 255, 0, .4);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.find-open .cm-searching.search-target-match {
|
|
||||||
background-color: darkorange;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.find-open .CodeMirror-search-match {
|
|
||||||
background: gold;
|
|
||||||
border-top: 1px solid orange;
|
|
||||||
border-bottom: 1px solid orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* hide default CM search highlight styling */
|
|
||||||
body .cm-searching,
|
|
||||||
body .CodeMirror-search-match {
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
#search-replace-dialog {
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
#search-replace-dialog textarea {
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
#search-replace-dialog[data-type="replace"] {
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
#search-replace-dialog[data-type="replace"] button:not(.hidden) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
#search-replace-dialog[data-type="replace"] button.hidden {
|
|
||||||
display: block !important;
|
|
||||||
font-size: 0;
|
|
||||||
width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#search-replace-dialog[data-type="replace"] button svg {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
#search-replace-dialog[data-type="replace"] textarea {
|
|
||||||
min-width: 50px;
|
|
||||||
max-width: calc(50vw - 120px);
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,934 +0,0 @@
|
||||||
/* global $ $$ $create $remove focusAccessibility setInputValue toggleDataset */// dom.js
|
|
||||||
/* global CodeMirror */
|
|
||||||
/* global chromeLocal */// storage-util.js
|
|
||||||
/* global colorMimicry */
|
|
||||||
/* global debounce stringAsRegExp tryRegExp */// toolbox.js
|
|
||||||
/* global editor */
|
|
||||||
/* global t */// localization.js
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
require(['/edit/global-search.css']);
|
|
||||||
|
|
||||||
//region Constants and state
|
|
||||||
|
|
||||||
const INCREMENTAL_SEARCH_DELAY = 0;
|
|
||||||
const ANNOTATE_SCROLLBAR_DELAY = 350;
|
|
||||||
const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3};
|
|
||||||
const STORAGE_UPDATE_DELAY = 500;
|
|
||||||
|
|
||||||
const DIALOG_SELECTOR = '#search-replace-dialog';
|
|
||||||
const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style';
|
|
||||||
const TARGET_CLASS = 'search-target-editor';
|
|
||||||
const MATCH_CLASS = 'search-target-match';
|
|
||||||
const MATCH_TOKEN_NAME = 'searching';
|
|
||||||
const APPLIES_VALUE_CLASS = 'applies-value';
|
|
||||||
|
|
||||||
const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
firstRun: true,
|
|
||||||
// used for case-sensitive matching directly
|
|
||||||
find: '',
|
|
||||||
// used when /re/ is detected or for case-insensitive matching
|
|
||||||
rx: null,
|
|
||||||
// used by overlay and doSearchInApplies, equals to rx || stringAsRegExp(find)
|
|
||||||
rx2: null,
|
|
||||||
|
|
||||||
icase: true,
|
|
||||||
reverse: false,
|
|
||||||
lastFind: '',
|
|
||||||
|
|
||||||
numFound: 0,
|
|
||||||
numApplies: -1,
|
|
||||||
|
|
||||||
replace: '',
|
|
||||||
lastReplace: null,
|
|
||||||
|
|
||||||
cm: null,
|
|
||||||
input: null,
|
|
||||||
input2: null,
|
|
||||||
dialog: null,
|
|
||||||
tally: null,
|
|
||||||
originalFocus: null,
|
|
||||||
|
|
||||||
undoHistory: [],
|
|
||||||
|
|
||||||
searchInApplies: !editor.isUsercss,
|
|
||||||
};
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
//region Events
|
|
||||||
|
|
||||||
const ACTIONS = {
|
|
||||||
key: {
|
|
||||||
'Enter': event => {
|
|
||||||
switch (document.activeElement) {
|
|
||||||
case state.input:
|
|
||||||
if (state.dialog.dataset.type === 'find') {
|
|
||||||
const found = doSearch({canAdvance: false});
|
|
||||||
if (found) {
|
|
||||||
const target = $('.' + TARGET_CLASS);
|
|
||||||
const cm = target.CodeMirror;
|
|
||||||
/* Since this runs in `keydown` event we have to delay focusing
|
|
||||||
* to prevent CodeMirror from seeing and handling the key */
|
|
||||||
setTimeout(() => (cm || target).focus());
|
|
||||||
if (cm) {
|
|
||||||
const {from, to} = cm.state.search.searchPos;
|
|
||||||
cm.jumpToPos(from, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroyDialog({restoreFocus: !found});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// fallthrough
|
|
||||||
case state.input2:
|
|
||||||
doReplace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return !focusAccessibility.closest(event.target);
|
|
||||||
},
|
|
||||||
'Esc': () => {
|
|
||||||
destroyDialog({restoreFocus: true});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
click: {
|
|
||||||
next: () => doSearch({reverse: false}),
|
|
||||||
prev: () => doSearch({reverse: true}),
|
|
||||||
close: () => destroyDialog({restoreFocus: true}),
|
|
||||||
replace: () => doReplace(),
|
|
||||||
replaceAll: () => doReplaceAll(),
|
|
||||||
undo: () => doUndo(),
|
|
||||||
clear() {
|
|
||||||
setInputValue(this._input, '');
|
|
||||||
},
|
|
||||||
case() {
|
|
||||||
state.icase = !state.icase;
|
|
||||||
state.lastFind = '';
|
|
||||||
toggleDataset(this, 'enabled', !state.icase);
|
|
||||||
doSearch({canAdvance: false});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const EVENTS = {
|
|
||||||
oninput() {
|
|
||||||
state.find = state.input.value;
|
|
||||||
debounce(doSearch, INCREMENTAL_SEARCH_DELAY, {canAdvance: false});
|
|
||||||
adjustTextareaSize(this);
|
|
||||||
if (!state.find) enableReplaceButtons(false);
|
|
||||||
},
|
|
||||||
onkeydown(event) {
|
|
||||||
const action = ACTIONS.key[CodeMirror.keyName(event)];
|
|
||||||
if (action && action(event) !== false) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclick(event) {
|
|
||||||
const el = event.target.closest('[data-action]');
|
|
||||||
const action = el && ACTIONS.click[el.dataset.action];
|
|
||||||
if (action && action.call(el, event) !== false) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onfocusout() {
|
|
||||||
if (!state.dialog.contains(document.activeElement)) {
|
|
||||||
state.dialog.on('focusin', EVENTS.onfocusin);
|
|
||||||
state.dialog.off('focusout', EVENTS.onfocusout);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onfocusin() {
|
|
||||||
state.dialog.on('focusout', EVENTS.onfocusout);
|
|
||||||
state.dialog.off('focusin', EVENTS.onfocusin);
|
|
||||||
trimUndoHistory();
|
|
||||||
enableUndoButton(state.undoHistory.length);
|
|
||||||
if (state.find) doSearch({canAdvance: false});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const DIALOG_PROPS = {
|
|
||||||
dialog: {
|
|
||||||
onclick: EVENTS.onclick,
|
|
||||||
onkeydown: EVENTS.onkeydown,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
oninput: EVENTS.oninput,
|
|
||||||
},
|
|
||||||
input2: {
|
|
||||||
oninput() {
|
|
||||||
state.replace = this.value;
|
|
||||||
adjustTextareaSize(this);
|
|
||||||
debounce(writeStorage, STORAGE_UPDATE_DELAY);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
//region Commands
|
|
||||||
|
|
||||||
const COMMANDS = {
|
|
||||||
find(cm, {reverse = false} = {}) {
|
|
||||||
state.reverse = reverse;
|
|
||||||
focusDialog('find', cm);
|
|
||||||
},
|
|
||||||
findNext: cm => doSearch({reverse: false, cm}),
|
|
||||||
findPrev: cm => doSearch({reverse: true, cm}),
|
|
||||||
replace(cm) {
|
|
||||||
state.reverse = false;
|
|
||||||
focusDialog('replace', cm);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
COMMANDS.replaceAll = COMMANDS.replace;
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
Object.assign(CodeMirror.commands, COMMANDS);
|
|
||||||
readStorage();
|
|
||||||
|
|
||||||
//region Find
|
|
||||||
|
|
||||||
function initState({initReplace} = {}) {
|
|
||||||
const text = state.find;
|
|
||||||
const textChanged = text !== state.lastFind;
|
|
||||||
if (textChanged) {
|
|
||||||
state.numFound = 0;
|
|
||||||
state.numApplies = -1;
|
|
||||||
state.lastFind = text;
|
|
||||||
const match = text && text.match(RX_MAYBE_REGEXP);
|
|
||||||
const unicodeFlag = 'unicode' in RegExp.prototype ? 'u' : '';
|
|
||||||
const string2regexpFlags = (state.icase ? 'gi' : 'g') + unicodeFlag;
|
|
||||||
state.rx = match && tryRegExp(match[1], 'g' + match[2].replace(/[guy]/g, '') + unicodeFlag) ||
|
|
||||||
text && (state.icase || text.includes('\n')) && stringAsRegExp(text, string2regexpFlags);
|
|
||||||
state.rx2 = state.rx || text && stringAsRegExp(text, string2regexpFlags);
|
|
||||||
state.cursorOptions = {
|
|
||||||
caseFold: !state.rx && state.icase,
|
|
||||||
multiline: true,
|
|
||||||
};
|
|
||||||
debounce(writeStorage, STORAGE_UPDATE_DELAY);
|
|
||||||
}
|
|
||||||
if (initReplace && state.replace !== state.lastReplace) {
|
|
||||||
state.lastReplace = state.replace;
|
|
||||||
state.replaceValue = state.replace.replace(/(\\r)?\\n/g, '\n').replace(/\\t/g, '\t');
|
|
||||||
state.replaceHasRefs = /\$[$&`'\d]/.test(state.replaceValue);
|
|
||||||
}
|
|
||||||
const cmFocused = document.activeElement && document.activeElement.closest('.CodeMirror');
|
|
||||||
state.activeAppliesTo = $(`.${APPLIES_VALUE_CLASS}:focus, .${APPLIES_VALUE_CLASS}.${TARGET_CLASS}`);
|
|
||||||
state.cmStart = editor.closestVisible(
|
|
||||||
cmFocused && document.activeElement ||
|
|
||||||
state.activeAppliesTo ||
|
|
||||||
state.cm);
|
|
||||||
const cmExtra = $('body > :not(#sections) .CodeMirror');
|
|
||||||
state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doSearch({
|
|
||||||
reverse = state.reverse,
|
|
||||||
canAdvance = true,
|
|
||||||
inApplies = true,
|
|
||||||
cm,
|
|
||||||
} = {}) {
|
|
||||||
if (cm) setActiveEditor(cm);
|
|
||||||
state.reverse = reverse;
|
|
||||||
if (!state.find && !dialogShown()) {
|
|
||||||
focusDialog('find', state.cm);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initState();
|
|
||||||
const {cmStart} = state;
|
|
||||||
const {index, found, foundInCode} = state.find && doSearchInEditors({cmStart, canAdvance, inApplies}) || {};
|
|
||||||
if (!foundInCode) clearMarker();
|
|
||||||
if (!found) makeTargetVisible(null);
|
|
||||||
const radiateFrom = foundInCode ? index : state.editors.indexOf(cmStart);
|
|
||||||
setupOverlay(radiateArray(state.editors, radiateFrom));
|
|
||||||
enableReplaceButtons(foundInCode);
|
|
||||||
if (state.find) {
|
|
||||||
const firstSuccessfulSearch = foundInCode && !state.numFound;
|
|
||||||
debounce(showTally, 0, firstSuccessfulSearch ? 1 : undefined);
|
|
||||||
} else {
|
|
||||||
showTally(0, 0);
|
|
||||||
}
|
|
||||||
state.firstRun = false;
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doSearchInEditors({cmStart, canAdvance, inApplies}) {
|
|
||||||
const query = state.rx || state.find;
|
|
||||||
const reverse = state.reverse;
|
|
||||||
const BOF = {line: 0, ch: 0};
|
|
||||||
const EOF = getEOF(cmStart);
|
|
||||||
|
|
||||||
const start = state.editors.indexOf(cmStart);
|
|
||||||
const total = state.editors.length;
|
|
||||||
let i = 0;
|
|
||||||
let wrapAround = 0;
|
|
||||||
let pos, index, cm;
|
|
||||||
|
|
||||||
if (inApplies && state.activeAppliesTo) {
|
|
||||||
if (doSearchInApplies(state.cmStart, canAdvance)) {
|
|
||||||
return {found: true};
|
|
||||||
}
|
|
||||||
if (reverse) pos = EOF; else i++;
|
|
||||||
} else {
|
|
||||||
pos = getContinuationPos({cm: cmStart, reverse: !canAdvance || reverse});
|
|
||||||
wrapAround = !reverse ?
|
|
||||||
CodeMirror.cmpPos(pos, BOF) > 0 :
|
|
||||||
CodeMirror.cmpPos(pos, EOF) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < total + wrapAround; i++) {
|
|
||||||
index = (start + i * (reverse ? -1 : 1) + total) % total;
|
|
||||||
cm = state.editors[index];
|
|
||||||
if (i) {
|
|
||||||
pos = !reverse ? BOF : {line: cm.doc.size, ch: 0};
|
|
||||||
}
|
|
||||||
const cursor = cm.getSearchCursor(query, pos, state.cursorOptions);
|
|
||||||
if (cursor.find(reverse)) {
|
|
||||||
makeMatchVisible(cm, cursor);
|
|
||||||
return {found: true, foundInCode: true, index};
|
|
||||||
}
|
|
||||||
const cmForNextApplies = !reverse ? cm : state.editors[index ? index - 1 : total - 1];
|
|
||||||
if (inApplies && doSearchInApplies(cmForNextApplies)) {
|
|
||||||
return {found: true};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doSearchInApplies(cm, canAdvance) {
|
|
||||||
if (!state.searchInApplies) return;
|
|
||||||
const inputs = editor.getSearchableInputs(cm);
|
|
||||||
if (state.reverse) inputs.reverse();
|
|
||||||
inputs.splice(0, inputs.indexOf(state.activeAppliesTo));
|
|
||||||
for (const input of inputs) {
|
|
||||||
const value = input.value;
|
|
||||||
if (input === state.activeAppliesTo) {
|
|
||||||
state.rx2.lastIndex = input.selectionStart + canAdvance;
|
|
||||||
} else {
|
|
||||||
state.rx2.lastIndex = 0;
|
|
||||||
}
|
|
||||||
const match = state.rx2.exec(value);
|
|
||||||
if (!match) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const end = match.index + match[0].length;
|
|
||||||
// scroll selected part into view in long inputs,
|
|
||||||
// works only outside of current event handlers chain, hence timeout=0
|
|
||||||
setTimeout(() => {
|
|
||||||
input.setSelectionRange(end, end);
|
|
||||||
input.setSelectionRange(match.index, end);
|
|
||||||
});
|
|
||||||
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
|
||||||
makeTargetVisible(!canFocus && input);
|
|
||||||
editor.scrollToEditor(cm);
|
|
||||||
if (canFocus) input.focus();
|
|
||||||
state.cm = cm;
|
|
||||||
clearMarker();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
//region Replace
|
|
||||||
|
|
||||||
function doReplace() {
|
|
||||||
initState({initReplace: true});
|
|
||||||
const cm = state.cmStart;
|
|
||||||
const generation = cm.changeGeneration();
|
|
||||||
const pos = getContinuationPos({cm, reverse: true});
|
|
||||||
const cursor = doReplaceInEditor({cm, pos});
|
|
||||||
if (!cursor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.findNext()) {
|
|
||||||
clearMarker();
|
|
||||||
makeMatchVisible(cm, cursor);
|
|
||||||
} else {
|
|
||||||
doSearchInEditors({cmStart: getNextEditor(cm)});
|
|
||||||
}
|
|
||||||
|
|
||||||
getStateSafe(cm).unclosedOp = false;
|
|
||||||
if (cm.curOp) cm.endOperation();
|
|
||||||
|
|
||||||
if (cursor) {
|
|
||||||
state.undoHistory.push([[cm, generation]]);
|
|
||||||
enableUndoButton(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doReplaceAll() {
|
|
||||||
initState({initReplace: true});
|
|
||||||
clearMarker();
|
|
||||||
const generations = new Map(state.editors.map(cm => [cm, cm.changeGeneration()]));
|
|
||||||
const found = state.editors.filter(cm => doReplaceInEditor({cm, all: true}));
|
|
||||||
if (found.length) {
|
|
||||||
state.lastFind = null;
|
|
||||||
state.undoHistory.push(found.map(cm => [cm, generations.get(cm)]));
|
|
||||||
enableUndoButton(true);
|
|
||||||
doSearch({canAdvance: false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doReplaceInEditor({cm, pos, all = false}) {
|
|
||||||
const cursor = cm.getSearchCursor(state.rx || state.find, pos, state.cursorOptions);
|
|
||||||
const replace = state.replaceValue;
|
|
||||||
let found;
|
|
||||||
|
|
||||||
cursor.find();
|
|
||||||
while (cursor.atOccurrence) {
|
|
||||||
found = true;
|
|
||||||
if (!cm.curOp) {
|
|
||||||
cm.startOperation();
|
|
||||||
getStateSafe(cm).unclosedOp = true;
|
|
||||||
}
|
|
||||||
if (state.rx) {
|
|
||||||
const text = cm.getRange(cursor.pos.from, cursor.pos.to);
|
|
||||||
cursor.replace(state.replaceHasRefs ? text.replace(state.rx, replace) : replace);
|
|
||||||
} else {
|
|
||||||
cursor.replace(replace);
|
|
||||||
}
|
|
||||||
if (!all) {
|
|
||||||
makeMatchVisible(cm, cursor);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
cursor.findNext();
|
|
||||||
}
|
|
||||||
if (all) {
|
|
||||||
getStateSafe(cm).searchPos = null;
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function doUndo() {
|
|
||||||
let undoneSome;
|
|
||||||
saveWindowScrollPos();
|
|
||||||
for (const [cm, generation] of state.undoHistory.pop() || []) {
|
|
||||||
if (document.body.contains(cm.display.wrapper) && !cm.isClean(generation)) {
|
|
||||||
cm.undo();
|
|
||||||
cm.getAllMarks().forEach(marker =>
|
|
||||||
marker !== state.marker &&
|
|
||||||
marker.className === MATCH_CLASS &&
|
|
||||||
marker.clear());
|
|
||||||
undoneSome = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enableUndoButton(state.undoHistory.length);
|
|
||||||
if (state.undoHistory.length) {
|
|
||||||
focusUndoButton();
|
|
||||||
} else {
|
|
||||||
state.input.focus();
|
|
||||||
}
|
|
||||||
if (undoneSome) {
|
|
||||||
state.lastFind = null;
|
|
||||||
restoreWindowScrollPos();
|
|
||||||
doSearch({
|
|
||||||
reverse: false,
|
|
||||||
canAdvance: false,
|
|
||||||
inApplies: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
//region Overlay
|
|
||||||
|
|
||||||
|
|
||||||
function setupOverlay(queue, debounced) {
|
|
||||||
if (!queue.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (queue.length > 1 || !debounced) {
|
|
||||||
debounce(setupOverlay, 0, queue, true);
|
|
||||||
if (!debounced) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let canContinue = true;
|
|
||||||
while (queue.length && canContinue) {
|
|
||||||
const cm = queue.shift();
|
|
||||||
if (!document.body.contains(cm.display.wrapper)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmState = getStateSafe(cm);
|
|
||||||
const query = state.rx2;
|
|
||||||
|
|
||||||
if ((cmState.overlay || {}).query === query) {
|
|
||||||
if (cmState.unclosedOp && cm.curOp) cm.endOperation();
|
|
||||||
cmState.unclosedOp = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmState.overlay) {
|
|
||||||
if (!cm.curOp) cm.startOperation();
|
|
||||||
cm.removeOverlay(cmState.overlay);
|
|
||||||
cmState.overlay = null;
|
|
||||||
canContinue = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMatches = query && cm.getSearchCursor(query, null, state.cursorOptions).find();
|
|
||||||
if (hasMatches) {
|
|
||||||
if (!cm.curOp) cm.startOperation();
|
|
||||||
cmState.overlay = {
|
|
||||||
query,
|
|
||||||
token: tokenize,
|
|
||||||
numFound: 0,
|
|
||||||
tallyShownTime: 0,
|
|
||||||
};
|
|
||||||
cm.addOverlay(cmState.overlay);
|
|
||||||
canContinue = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmState.annotate) {
|
|
||||||
if (!cm.curOp) cm.startOperation();
|
|
||||||
cmState.annotate.clear();
|
|
||||||
cmState.annotate = null;
|
|
||||||
canContinue = false;
|
|
||||||
}
|
|
||||||
if (cmState.annotateTimer) {
|
|
||||||
clearTimeout(cmState.annotateTimer);
|
|
||||||
cmState.annotateTimer = 0;
|
|
||||||
}
|
|
||||||
if (hasMatches) {
|
|
||||||
cmState.annotateTimer = setTimeout(annotateScrollbar, ANNOTATE_SCROLLBAR_DELAY,
|
|
||||||
cm, query, state.icase);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmState.unclosedOp = false;
|
|
||||||
if (cm.curOp) cm.endOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!queue.length) debounce.unregister(setupOverlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function tokenize(stream) {
|
|
||||||
this.query.lastIndex = stream.pos;
|
|
||||||
const match = this.query.exec(stream.string);
|
|
||||||
if (match && match.index === stream.pos) {
|
|
||||||
this.numFound++;
|
|
||||||
const t = performance.now();
|
|
||||||
if (t - this.tallyShownTime > 10) {
|
|
||||||
this.tallyShownTime = t;
|
|
||||||
debounce(showTally);
|
|
||||||
}
|
|
||||||
stream.pos += match[0].length || 1;
|
|
||||||
return MATCH_TOKEN_NAME;
|
|
||||||
} else if (match) {
|
|
||||||
stream.pos = match.index;
|
|
||||||
} else {
|
|
||||||
stream.skipToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function annotateScrollbar(cm, query, icase) {
|
|
||||||
getStateSafe(cm).annotate = cm.showMatchesOnScrollbar(query, icase, ANNOTATE_SCROLLBAR_OPTIONS);
|
|
||||||
debounce(showTally);
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
//region Dialog
|
|
||||||
|
|
||||||
function focusDialog(type, cm) {
|
|
||||||
setActiveEditor(cm);
|
|
||||||
|
|
||||||
const dialogFocused = state.dialog && state.dialog.contains(document.activeElement);
|
|
||||||
let sel = dialogFocused ? '' : getSelection().toString() || cm && cm.getSelection();
|
|
||||||
sel = !sel.includes('\n') && !sel.includes('\r') && sel;
|
|
||||||
if (sel) state.find = sel;
|
|
||||||
|
|
||||||
if (!dialogShown(type)) {
|
|
||||||
destroyDialog();
|
|
||||||
createDialog(type);
|
|
||||||
} else if (sel) {
|
|
||||||
setInputValue(state.input, sel);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.input.focus();
|
|
||||||
state.input.select();
|
|
||||||
if (state.find) {
|
|
||||||
doSearch({canAdvance: false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function dialogShown(type) {
|
|
||||||
return document.body.contains(state.input) &&
|
|
||||||
(!type || state.dialog.dataset.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createDialog(type) {
|
|
||||||
state.originalFocus = document.activeElement;
|
|
||||||
state.firstRun = true;
|
|
||||||
|
|
||||||
const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
|
|
||||||
Object.assign(dialog, DIALOG_PROPS.dialog);
|
|
||||||
dialog.on('focusout', EVENTS.onfocusout);
|
|
||||||
dialog.dataset.type = type;
|
|
||||||
dialog.style.pointerEvents = 'auto';
|
|
||||||
|
|
||||||
const content = $('[data-type="content"]', dialog);
|
|
||||||
content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
|
|
||||||
|
|
||||||
createInput(0, 'input', state.find);
|
|
||||||
createInput(1, 'input2', state.replace);
|
|
||||||
toggleDataset($('[data-action="case"]', dialog), 'enabled', !state.icase);
|
|
||||||
state.tally = $('[data-type="tally"]', dialog);
|
|
||||||
|
|
||||||
const colors = {
|
|
||||||
body: colorMimicry(document.body, {bg: 'backgroundColor'}),
|
|
||||||
input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
|
|
||||||
icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
|
|
||||||
};
|
|
||||||
$.root.appendChild(
|
|
||||||
$(DIALOG_STYLE_SELECTOR) ||
|
|
||||||
$create('style' + DIALOG_STYLE_SELECTOR)
|
|
||||||
).textContent = `
|
|
||||||
#search-replace-dialog {
|
|
||||||
background-color: ${colors.body.bg};
|
|
||||||
}
|
|
||||||
#search-replace-dialog textarea {
|
|
||||||
color: ${colors.body.fore};
|
|
||||||
background-color: ${colors.input.bg};
|
|
||||||
}
|
|
||||||
#search-replace-dialog svg {
|
|
||||||
fill: ${colors.icon.fill};
|
|
||||||
}
|
|
||||||
#search-replace-dialog [data-action="case"] {
|
|
||||||
color: ${colors.icon.fill};
|
|
||||||
}
|
|
||||||
#search-replace-dialog[data-type="replace"] button:hover svg,
|
|
||||||
#search-replace-dialog svg:hover {
|
|
||||||
fill: var(--cmin);
|
|
||||||
}
|
|
||||||
#search-replace-dialog [data-action="case"]:hover {
|
|
||||||
color: var(--cmin);
|
|
||||||
}
|
|
||||||
#search-replace-dialog [data-action="clear"] {
|
|
||||||
background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(dialog);
|
|
||||||
dispatchEvent(new Event('showHotkeyInTooltip'));
|
|
||||||
|
|
||||||
adjustTextareaSize(state.input);
|
|
||||||
if (type === 'replace') {
|
|
||||||
adjustTextareaSize(state.input2);
|
|
||||||
enableReplaceButtons(state.find !== '');
|
|
||||||
enableUndoButton(state.undoHistory.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createInput(index, name, value) {
|
|
||||||
const input = state[name] = $$('textarea', state.dialog)[index];
|
|
||||||
if (!input) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
input.value = value;
|
|
||||||
Object.assign(input, DIALOG_PROPS[name]);
|
|
||||||
|
|
||||||
input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
|
|
||||||
$('[data-action]', input.parentElement)._input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function destroyDialog({restoreFocus = false} = {}) {
|
|
||||||
state.input = null;
|
|
||||||
$remove(DIALOG_SELECTOR);
|
|
||||||
debounce.unregister(doSearch);
|
|
||||||
makeTargetVisible(null);
|
|
||||||
if (restoreFocus) {
|
|
||||||
setTimeout(focusNoScroll, 0, state.originalFocus);
|
|
||||||
} else {
|
|
||||||
saveWindowScrollPos();
|
|
||||||
restoreWindowScrollPos({immediately: false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function adjustTextareaSize(el) {
|
|
||||||
const oldWidth = parseFloat(el.style.width) || el.clientWidth;
|
|
||||||
const widthHistory = el._widthHistory = el._widthHistory || new Map();
|
|
||||||
const knownWidth = widthHistory.get(el.value);
|
|
||||||
let newWidth;
|
|
||||||
if (knownWidth) {
|
|
||||||
newWidth = knownWidth;
|
|
||||||
} else {
|
|
||||||
const hasVerticalScrollbar = el.scrollHeight > el.clientHeight;
|
|
||||||
newWidth = el.scrollWidth + (hasVerticalScrollbar ? el.scrollWidth - el.clientWidth : 0);
|
|
||||||
newWidth += newWidth > oldWidth ? 50 : 0;
|
|
||||||
widthHistory.set(el.value, newWidth);
|
|
||||||
}
|
|
||||||
if (newWidth !== oldWidth) {
|
|
||||||
const dialogRightOffset = parseFloat(getComputedStyle(state.dialog).right);
|
|
||||||
const dialogRight = state.dialog.getBoundingClientRect().right;
|
|
||||||
const textRight = (state.input2 || state.input).getBoundingClientRect().right;
|
|
||||||
newWidth = Math.min(newWidth,
|
|
||||||
(window.innerWidth - dialogRightOffset - (dialogRight - textRight)) / (state.input2 ? 2 : 1) - 20);
|
|
||||||
el.style.width = newWidth + 'px';
|
|
||||||
}
|
|
||||||
const numLines = el.value.split('\n').length;
|
|
||||||
if (numLines !== Number(el.rows)) {
|
|
||||||
el.rows = numLines;
|
|
||||||
}
|
|
||||||
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function enableReplaceButtons(enabled) {
|
|
||||||
if (state.dialog && state.dialog.dataset.type === 'replace') {
|
|
||||||
for (const el of $$('[data-action^="replace"]', state.dialog)) {
|
|
||||||
el.disabled = !enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function enableUndoButton(enabled) {
|
|
||||||
if (state.dialog && state.dialog.dataset.type === 'replace') {
|
|
||||||
for (const el of $$('[data-action="undo"]', state.dialog)) {
|
|
||||||
el.disabled = !enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function focusUndoButton() {
|
|
||||||
for (const btn of $$('[data-action="undo"]', state.dialog)) {
|
|
||||||
if (getComputedStyle(btn).display !== 'none') {
|
|
||||||
btn.focus();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
//region Utility
|
|
||||||
|
|
||||||
function getStateSafe(cm) {
|
|
||||||
return cm.state.search || (cm.state.search = {});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// determines search start position:
|
|
||||||
// the cursor if it was moved or the last match
|
|
||||||
function getContinuationPos({cm, reverse}) {
|
|
||||||
const cmSearchState = getStateSafe(cm);
|
|
||||||
const posType = reverse ? 'from' : 'to';
|
|
||||||
const searchPos = (cmSearchState.searchPos || {})[posType];
|
|
||||||
const cursorPos = cm.getCursor(posType);
|
|
||||||
const preferCursor = !searchPos || CodeMirror.cmpPos(cursorPos, cmSearchState.cursorPos[posType]);
|
|
||||||
return preferCursor ? cursorPos : searchPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getEOF(cm) {
|
|
||||||
const line = cm.doc.size - 1;
|
|
||||||
return {line, ch: cm.getLine(line).length};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getNextEditor(cm, step = 1) {
|
|
||||||
const editors = state.editors;
|
|
||||||
return editors[(editors.indexOf(cm) + step + editors.length) % editors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// sets the editor to start the search in
|
|
||||||
// e.g. when the user switched to another editor and invoked a search command
|
|
||||||
function setActiveEditor(cm) {
|
|
||||||
if (cm.display.wrapper.contains(document.activeElement)) {
|
|
||||||
state.cm = cm;
|
|
||||||
state.originalFocus = cm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// adds a class on the editor that contains the active match
|
|
||||||
// instead of focusing it (in order to keep the minidialog focused)
|
|
||||||
function makeTargetVisible(element) {
|
|
||||||
const old = $('.' + TARGET_CLASS);
|
|
||||||
if (old !== element) {
|
|
||||||
if (old) {
|
|
||||||
old.classList.remove(TARGET_CLASS);
|
|
||||||
document.body.classList.remove('find-open');
|
|
||||||
}
|
|
||||||
if (element) {
|
|
||||||
element.classList.add(TARGET_CLASS);
|
|
||||||
document.body.classList.add('find-open');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// scrolls the editor to reveal the match
|
|
||||||
function makeMatchVisible(cm, searchCursor) {
|
|
||||||
const canFocus = !state.firstRun && (!state.dialog || !state.dialog.contains(document.activeElement));
|
|
||||||
state.cm = cm;
|
|
||||||
// scroll within the editor
|
|
||||||
const pos = searchCursor.pos;
|
|
||||||
Object.assign(getStateSafe(cm), {
|
|
||||||
cursorPos: {
|
|
||||||
from: cm.getCursor('from'),
|
|
||||||
to: cm.getCursor('to'),
|
|
||||||
},
|
|
||||||
searchPos: pos,
|
|
||||||
unclosedOp: !cm.curOp,
|
|
||||||
});
|
|
||||||
if (!cm.curOp) cm.startOperation();
|
|
||||||
if (!state.firstRun) {
|
|
||||||
cm.jumpToPos(pos.from, pos.to);
|
|
||||||
}
|
|
||||||
// focus or expose as the current search target
|
|
||||||
clearMarker();
|
|
||||||
if (canFocus) {
|
|
||||||
cm.focus();
|
|
||||||
makeTargetVisible(null);
|
|
||||||
} else {
|
|
||||||
makeTargetVisible(cm.display.wrapper);
|
|
||||||
// mark the match
|
|
||||||
state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, {
|
|
||||||
className: MATCH_CLASS,
|
|
||||||
clearOnEnter: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function clearMarker() {
|
|
||||||
if (state.marker) state.marker.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function showTally(num, numApplies) {
|
|
||||||
if (!state.tally) return;
|
|
||||||
if (num === undefined) {
|
|
||||||
num = 0;
|
|
||||||
for (const cm of state.editors) {
|
|
||||||
const {annotate, overlay} = getStateSafe(cm);
|
|
||||||
num +=
|
|
||||||
((annotate || {}).matches || []).length ||
|
|
||||||
(overlay || {}).numFound ||
|
|
||||||
0;
|
|
||||||
}
|
|
||||||
state.numFound = num;
|
|
||||||
}
|
|
||||||
if (numApplies === undefined && state.searchInApplies && state.numApplies < 0) {
|
|
||||||
numApplies = 0;
|
|
||||||
const elements = state.find ? document.getElementsByClassName(APPLIES_VALUE_CLASS) : [];
|
|
||||||
const {rx} = state;
|
|
||||||
for (const el of elements) {
|
|
||||||
const value = el.value;
|
|
||||||
if (rx) {
|
|
||||||
rx.lastIndex = 0;
|
|
||||||
// preventing an infinite loop if matched an empty string and didn't advance
|
|
||||||
for (let m; (m = rx.exec(value)) && ++numApplies && rx.lastIndex > m.index;) { /* NOP */ }
|
|
||||||
} else {
|
|
||||||
let i = -1;
|
|
||||||
while ((i = value.indexOf(state.find, i + 1)) >= 0) numApplies++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.numApplies = numApplies;
|
|
||||||
} else {
|
|
||||||
numApplies = state.numApplies;
|
|
||||||
}
|
|
||||||
const newText = num + (numApplies > 0 ? '+' + numApplies : '');
|
|
||||||
if (state.tally.textContent !== newText) {
|
|
||||||
state.tally.textContent = newText;
|
|
||||||
const newTitle = t('searchNumberOfResults' + (numApplies ? '2' : ''));
|
|
||||||
if (state.tally.title !== newTitle) state.tally.title = newTitle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function trimUndoHistory() {
|
|
||||||
const history = state.undoHistory;
|
|
||||||
for (let last; (last = history[history.length - 1]);) {
|
|
||||||
const undoables = last.filter(([cm, generation]) =>
|
|
||||||
document.body.contains(cm.display.wrapper) && !cm.isClean(generation));
|
|
||||||
if (undoables.length) {
|
|
||||||
history[history.length - 1] = undoables;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
history.length--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function focusNoScroll(el) {
|
|
||||||
if (el) {
|
|
||||||
saveWindowScrollPos();
|
|
||||||
el.focus({preventScroll: true});
|
|
||||||
restoreWindowScrollPos({immediately: false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function saveWindowScrollPos() {
|
|
||||||
state.scrollX = window.scrollX;
|
|
||||||
state.scrollY = window.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function restoreWindowScrollPos({immediately = true} = {}) {
|
|
||||||
if (!immediately) {
|
|
||||||
// run in the next microtask cycle
|
|
||||||
Promise.resolve().then(restoreWindowScrollPos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.scrollX !== state.scrollX || window.scrollY !== state.scrollY) {
|
|
||||||
window.scrollTo(state.scrollX, state.scrollY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
|
|
||||||
function radiateArray(arr, focalIndex) {
|
|
||||||
const focus = arr[focalIndex];
|
|
||||||
if (!focus) return arr;
|
|
||||||
const result = [focus];
|
|
||||||
const len = arr.length;
|
|
||||||
for (let i = 1; i < len; i++) {
|
|
||||||
if (focalIndex + i < len) {
|
|
||||||
result.push(arr[focalIndex + i]);
|
|
||||||
}
|
|
||||||
if (focalIndex - i >= 0) {
|
|
||||||
result.push(arr[focalIndex - i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function readStorage() {
|
|
||||||
chromeLocal.getValue('editor').then((editor = {}) => {
|
|
||||||
state.find = editor.find || '';
|
|
||||||
state.replace = editor.replace || '';
|
|
||||||
state.icase = editor.icase || state.icase;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function writeStorage() {
|
|
||||||
chromeLocal.getValue('editor').then((editor = {}) =>
|
|
||||||
chromeLocal.setValue('editor', Object.assign(editor, {
|
|
||||||
find: state.find,
|
|
||||||
replace: state.replace,
|
|
||||||
icase: state.icase,
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
})();
|
|
43
edit/lint-codemirror-helper.js
Normal file
43
edit/lint-codemirror-helper.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/* global CodeMirror linterConfig */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
CodeMirror.registerHelper('lint', 'csslint', invokeHelper);
|
||||||
|
CodeMirror.registerHelper('lint', 'stylelint', invokeHelper);
|
||||||
|
|
||||||
|
const cookResults = {
|
||||||
|
csslint: results =>
|
||||||
|
results.map(({line, col: ch, message, rule, type: severity}) => line && {
|
||||||
|
message,
|
||||||
|
from: {line: line - 1, ch: ch - 1},
|
||||||
|
to: {line: line - 1, ch},
|
||||||
|
rule: rule.id,
|
||||||
|
severity,
|
||||||
|
}).filter(Boolean),
|
||||||
|
|
||||||
|
stylelint: ({results}) =>
|
||||||
|
!results[0] && [] ||
|
||||||
|
results[0].warnings.map(({line, column: ch, text, severity}) => ({
|
||||||
|
from: {line: line - 1, ch: ch - 1},
|
||||||
|
to: {line: line - 1, ch},
|
||||||
|
message: text
|
||||||
|
.replace('Unexpected ', '')
|
||||||
|
.replace(/^./, firstLetter => firstLetter.toUpperCase())
|
||||||
|
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
|
||||||
|
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
|
||||||
|
severity,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
function invokeHelper(code, options, cm) {
|
||||||
|
const config = linterConfig.getCurrent();
|
||||||
|
return linterConfig.invokeWorker({code, config})
|
||||||
|
.then(cookResults[linterConfig.getName()])
|
||||||
|
.then(results => {
|
||||||
|
if (options && typeof options.preUpdateLinting === 'function') {
|
||||||
|
options.preUpdateLinting(cm);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
49
edit/lint-defaults-csslint.js
Normal file
49
edit/lint-defaults-csslint.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSSLint Config values
|
||||||
|
* 0 = disabled; 1 = warning; 2 = error
|
||||||
|
*/
|
||||||
|
window.linterConfig.defaults.csslint = {
|
||||||
|
// Default warnings
|
||||||
|
'display-property-grouping': 1,
|
||||||
|
'duplicate-properties': 1,
|
||||||
|
'empty-rules': 1,
|
||||||
|
'errors': 1,
|
||||||
|
'known-properties': 1,
|
||||||
|
|
||||||
|
// Default disabled
|
||||||
|
'adjoining-classes': 0,
|
||||||
|
'box-model': 0,
|
||||||
|
'box-sizing': 0,
|
||||||
|
'bulletproof-font-face': 0,
|
||||||
|
'compatible-vendor-prefixes': 0,
|
||||||
|
'duplicate-background-images': 0,
|
||||||
|
'fallback-colors': 0,
|
||||||
|
'floats': 0,
|
||||||
|
'font-faces': 0,
|
||||||
|
'font-sizes': 0,
|
||||||
|
'gradients': 0,
|
||||||
|
'ids': 0,
|
||||||
|
'import': 0,
|
||||||
|
'import-ie-limit': 0,
|
||||||
|
'important': 0,
|
||||||
|
'order-alphabetical': 0,
|
||||||
|
'outline-none': 0,
|
||||||
|
'overqualified-elements': 0,
|
||||||
|
'qualified-headings': 0,
|
||||||
|
'regex-selectors': 0,
|
||||||
|
'rules-count': 0,
|
||||||
|
'selector-max': 0,
|
||||||
|
'selector-max-approaching': 0,
|
||||||
|
'selector-newline': 0,
|
||||||
|
'shorthand': 0,
|
||||||
|
'star-property-hack': 0,
|
||||||
|
'text-indent': 0,
|
||||||
|
'underscore-property-hack': 0,
|
||||||
|
'unique-headings': 0,
|
||||||
|
'universal-selector': 0,
|
||||||
|
'unqualified-attributes': 0,
|
||||||
|
'vendor-prefix': 0,
|
||||||
|
'zero-units': 0
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user