Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ec4e0a69f2 | ||
|
52ae781960 | ||
|
c2f993db27 | ||
|
9f9b8be0f4 |
|
@ -1,2 +1,3 @@
|
|||
vendor/
|
||||
vendor-overwrites/
|
||||
beautify/
|
||||
codemirror/
|
||||
csslint/
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2017
|
||||
ecmaVersion: 2015
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
@ -9,7 +9,51 @@ env:
|
|||
webextensions: true
|
||||
|
||||
globals:
|
||||
require: readonly # in polyfill.js
|
||||
# messaging.js
|
||||
KEEP_CHANNEL_OPEN: false
|
||||
FIREFOX: false
|
||||
OPERA: false
|
||||
URLS: false
|
||||
BG: false
|
||||
notifyAllTabs: false
|
||||
getTab: 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
|
||||
# localization.js
|
||||
template: false
|
||||
t: false
|
||||
o: false
|
||||
tE: false
|
||||
tHTML: false
|
||||
tNodeList: false
|
||||
tDocLoader: false
|
||||
# dom.js
|
||||
onDOMready: false
|
||||
scrollElementIntoView: false
|
||||
enforceInputRange: false
|
||||
animateElement: false
|
||||
$: false
|
||||
$$: false
|
||||
$element: false
|
||||
# prefs.js
|
||||
prefs: false
|
||||
setupLivePrefs: false
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
|
@ -22,19 +66,19 @@ rules:
|
|||
brace-style: [2, 1tbs, {allowSingleLine: false}]
|
||||
camelcase: [2, {properties: never}]
|
||||
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-style: [2, last]
|
||||
complexity: [0]
|
||||
computed-property-spacing: [2, never]
|
||||
consistent-return: [0]
|
||||
constructor-super: [2]
|
||||
curly: [2, "multi-line"]
|
||||
curly: [2]
|
||||
default-case: [0]
|
||||
dot-location: [2, property]
|
||||
dot-notation: [0]
|
||||
eol-last: [2]
|
||||
eqeqeq: [1, smart]
|
||||
eqeqeq: [0]
|
||||
func-call-spacing: [2, never]
|
||||
func-name-matching: [0]
|
||||
func-names: [0]
|
||||
|
@ -45,15 +89,7 @@ rules:
|
|||
id-blacklist: [0]
|
||||
id-length: [0]
|
||||
id-match: [0]
|
||||
indent: [2, 2, {
|
||||
SwitchCase: 1,
|
||||
ignoreComments: true,
|
||||
ignoredNodes: [
|
||||
"TemplateLiteral > *",
|
||||
"ConditionalExpression",
|
||||
"ForStatement"
|
||||
]
|
||||
}]
|
||||
indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
|
||||
jsx-quotes: [0]
|
||||
key-spacing: [0]
|
||||
keyword-spacing: [2]
|
||||
|
@ -77,11 +113,11 @@ rules:
|
|||
no-case-declarations: [2]
|
||||
no-class-assign: [2]
|
||||
no-cond-assign: [2, except-parens]
|
||||
no-confusing-arrow: [0, {allowParens: true}]
|
||||
no-confusing-arrow: [1, {allowParens: true}]
|
||||
no-const-assign: [2]
|
||||
no-constant-condition: [0]
|
||||
no-continue: [0]
|
||||
no-control-regex: [0]
|
||||
no-control-regex: [2]
|
||||
no-debugger: [2]
|
||||
no-delete-var: [2]
|
||||
no-div-regex: [0]
|
||||
|
@ -95,9 +131,9 @@ rules:
|
|||
no-empty-function: [0]
|
||||
no-empty-pattern: [2]
|
||||
no-empty: [2, {allowEmptyCatch: true}]
|
||||
no-eq-null: [0]
|
||||
no-eq-null: [2]
|
||||
no-eval: [2]
|
||||
no-ex-assign: [0]
|
||||
no-ex-assign: [2]
|
||||
no-extend-native: [2]
|
||||
no-extra-bind: [2]
|
||||
no-extra-boolean-cast: [2]
|
||||
|
@ -126,7 +162,7 @@ rules:
|
|||
no-mixed-operators: [0]
|
||||
no-mixed-requires: [2, true]
|
||||
no-mixed-spaces-and-tabs: [2]
|
||||
no-multi-spaces: [2, {ignoreEOLComments: true}]
|
||||
no-multi-spaces: [0]
|
||||
no-multi-str: [2]
|
||||
no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}]
|
||||
no-native-reassign: [2]
|
||||
|
@ -147,9 +183,6 @@ rules:
|
|||
no-proto: [2]
|
||||
no-redeclare: [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-modules: [2, domain, freelist, smalloc, sys]
|
||||
no-restricted-syntax: [2, WithStatement]
|
||||
|
@ -170,16 +203,17 @@ rules:
|
|||
no-trailing-spaces: [2]
|
||||
no-undef-init: [2]
|
||||
no-undef: [2]
|
||||
no-undefined: [0]
|
||||
no-underscore-dangle: [0]
|
||||
no-unexpected-multiline: [2]
|
||||
no-unmodified-loop-condition: [0]
|
||||
no-unmodified-loop-condition: [1]
|
||||
no-unneeded-ternary: [2]
|
||||
no-unreachable: [2]
|
||||
no-unsafe-finally: [2]
|
||||
no-unsafe-negation: [2]
|
||||
no-unused-expressions: [2]
|
||||
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-useless-call: [2]
|
||||
no-useless-computed-key: [2]
|
||||
|
@ -194,16 +228,16 @@ rules:
|
|||
object-curly-spacing: [2, never]
|
||||
object-shorthand: [0]
|
||||
one-var-declaration-per-line: [1]
|
||||
one-var: [2, {initialized: never}]
|
||||
one-var: [0]
|
||||
operator-assignment: [2, always]
|
||||
operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
|
||||
padded-blocks: [0]
|
||||
padded-blocks: [2, never]
|
||||
prefer-numeric-literals: [2]
|
||||
prefer-rest-params: [0]
|
||||
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
|
||||
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}]
|
||||
quote-props: [0]
|
||||
quotes: [1, single, avoid-escape]
|
||||
radix: [2, always]
|
||||
radix: [2, as-needed]
|
||||
require-jsdoc: [0]
|
||||
require-yield: [2]
|
||||
semi-spacing: [2, {before: false, after: true}]
|
||||
|
@ -211,7 +245,7 @@ rules:
|
|||
sort-imports: [0]
|
||||
sort-keys: [0]
|
||||
space-before-blocks: [2, always]
|
||||
space-before-function-paren: [2, {anonymous: always, asyncArrow: always, named: never}]
|
||||
space-before-function-paren: [2, never]
|
||||
space-in-parens: [2, never]
|
||||
space-infix-ops: [2]
|
||||
space-unary-ops: [2]
|
||||
|
@ -225,16 +259,3 @@ rules:
|
|||
wrap-iife: [2, inside]
|
||||
yield-star-spacing: [2, {before: true, after: false}]
|
||||
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
|
46
.github/CONTRIBUTING.md
vendored
46
.github/CONTRIBUTING.md
vendored
|
@ -1,46 +0,0 @@
|
|||
# Contributing to Stylus
|
||||
|
||||
1. [Getting involved](#getting-involved)
|
||||
2. [How to report issues](#how-to-report-issues)
|
||||
3. [Adding translations](#adding-translations)
|
||||
4. [Pull requests](#pull-requests)
|
||||
5. [Scripts](#scripts)
|
||||
6. [Updating locale files](#updating-locale-files-admin-only)
|
||||
7. [Contact us](#contact-us)
|
||||
|
||||
## 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.
|
||||
|
||||
## 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 adding a **feature request**, please search through the existing issues to see if it the feature has already been requested, added or rejected.
|
||||
|
||||
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).
|
||||
Only the languages supported by the web store are allowed:
|
||||
https://developer.chrome.com/docs/webstore/i18n/#localeTable
|
||||
|
||||
|
||||
## Pull requests
|
||||
|
||||
* First open an issue to discuss your changes.
|
||||
* Then download, fork or clone this repository.
|
||||
<!-- * Use [node.js](https://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).
|
||||
* 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.
|
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
|
||||
.eslintcache
|
||||
.transifexrc
|
||||
.vscode
|
||||
desktop.ini
|
||||
node_modules/
|
||||
yarn.lock
|
||||
pull_locales_login.rb
|
||||
|
|
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.
|
55
README.md
55
README.md
|
@ -1,58 +1,31 @@
|
|||
# Stylus
|
||||
|
||||
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.
|
||||
Stylus : Stylish fork for Chrome and Chrome-like things
|
||||
|
||||
## Releases
|
||||
|
||||
1. [Chrome Web Store](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne) (or [beta](https://chrome.google.com/webstore/detail/stylus-beta/apmmpaebfobifelkijhaljbmpcgbjbdo))
|
||||
2. [Firefox add-ons](https://addons.mozilla.org/firefox/addon/styl-us/)
|
||||
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)
|
||||
1. [Chrome Web Store](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne)
|
||||
2. [Opera add-ons](https://addons.opera.com/extensions/details/stylus/)
|
||||
|
||||
## Screenshots
|
||||
## Screen shot
|
||||
|
||||
Manager | Editor | Popup search | Popup config | Manager config | Options
|
||||
-|-|-|-|-|-
|
||||
![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)
|
||||
![screen shot](https://cloud.githubusercontent.com/assets/11704051/24002324/aefd19fe-0a75-11e7-8160-d8731d2a6d03.png)
|
||||
|
||||
## Help
|
||||
|
||||
* [Stylus help and FAQ in our Wiki](https://github.com/openstyles/stylus/wiki)
|
||||
* [Discussion section](https://add0n.com/stylus.html#reviews) of our representation on add0n.com
|
||||
* Discord: [![Discord][chat-image]][chat-link]
|
||||
|
||||
[chat-image]: https://img.shields.io/discord/379521691774353408.svg
|
||||
[chat-link]: https://discordapp.com/widget?id=379521691774353408
|
||||
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.
|
||||
|
||||
## Contributing
|
||||
|
||||
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/schomery/stylish-chrome) and pull requests are welcome.
|
||||
|
||||
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus).
|
||||
|
||||
See our [contributing](./.github/CONTRIBUTING.md) page for more details.
|
||||
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 [stylish-chrome/issues](https://github.com/schomery/stylish-chrome/issues).
|
||||
|
||||
## License
|
||||
|
||||
Inherited code from the original [Stylish](https://github.com/stylish-userstyles/stylish/):
|
||||
For copyright status of the "codemirror" directory, see 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 © 2017-2022 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
|
||||
|
||||
**[GNU GPLv3](./LICENSE)**
|
||||
Copyright (C) 2017 Stylus Team
|
||||
|
||||
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
|
||||
|
@ -65,8 +38,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://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.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
|
|
@ -1,178 +1,417 @@
|
|||
{
|
||||
"InaccessibleFileHint": {
|
||||
"message": "Stylus لا يستطيع الوصول الى بعض انواع الملفات ( ملفات pdf و json )"
|
||||
"appliesToEverything": {
|
||||
"message": "كل شيء",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"addStyleLabel": {
|
||||
"message": "كتابة نمط جديد"
|
||||
"defaultTheme": {
|
||||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "إضافة نمط"
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"issues": {
|
||||
"message": "Issues",
|
||||
"description": "Label for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Tab size",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "تمكين",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "أدخل اسمًا.",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "عناوين URL في النطاق",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "البحث عن تحديث",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Append to style",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "All styles are up to date.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Paste the Mozilla-format code",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "مساعدة",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Yes",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"findStylesForSite": {
|
||||
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا.",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "الأنماط المثبتة",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Beautify",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "ممكّن",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "إضافة قسم آخر",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "حفظ",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Stop",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "this URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "إضافة"
|
||||
"message": "إضافة",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "عناوين URL التي تطابق regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Search contents",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "تعطيل",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Show number of styles active for the current site on the toolbar button",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Show active style count",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Word wrap",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "رجوع للإدارة",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Import",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Filters",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Apply all updates",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "هل تريد بالتأكيد حذف هذا النمط؟",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Cancel"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp is invalid.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Options",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "ينطبق على: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "و المزيد"
|
||||
"styleUpdate": {
|
||||
"message": "Are you sure you want to update '$stylename$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "عناوين URL في النطاق"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم."
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "ينطبق على"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "عناوين URL التي تطابق regexp"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "إزالة"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "تحديد"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "كل شيء"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "عنوان URL"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "عناوين URL البادئة بـ"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "البحث عن تحديثات لكل الأنماط"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "البحث عن تحديث"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "جارٍ البحث..."
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "حذف"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "حفظ"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "هل تريد بالتأكيد حذف هذا النمط؟"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "حذف"
|
||||
},
|
||||
"description": {
|
||||
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى."
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "تعطيل"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "حذف"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "تعديل النمط"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "تعديل"
|
||||
"styleSectionsTitle": {
|
||||
"message": "الأقسام",
|
||||
"description": "Title for the style sections section"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "تعديل النمط $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "تمكين"
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "النمط محدّث.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "إضافة"
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "عناوين URL البادئة بـ",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "ممكّن"
|
||||
"searchRegexp": {
|
||||
"message": "Use /re/ syntax for regexp search",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "مساعدة"
|
||||
"importReplaceTooltip": {
|
||||
"message": "Discard contents of current style and overwrite it with the imported style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "تثبيت التحديث"
|
||||
"popupStylesFirst": {
|
||||
"message": "List styles before commands in the toolbar button menu",
|
||||
"description": "Label for the checkbox controlling section order in the toolbar button menu."
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "الأنماط المثبتة"
|
||||
"sectionHelp": {
|
||||
"message": "تتيح لك الأقسام تحديد أجزاء مختلفة من الرمز لتطبيقها على مجموعات مختلفة من عناوين URL بالنمط نفسه. فعلى سبيل المثال، يمكن لنمط مفرد تغيير الصفحة الرئيسية لموقع ويب بطريقة، مع تغيير بقية أجزاء موقع الويب بطريقة أخرى.",
|
||||
"description": "Help text for sections"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا."
|
||||
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "والمزيد",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "إزالة",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Style in Mozilla format",
|
||||
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Stylus",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Write style for: ",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"replace": {
|
||||
"message": "Replace",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "ينطبق على",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "إدارة الأنماط المثبتة"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "عنوان URL"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "إضافة قسم آخر"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "الرمز"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "إزالة القسم"
|
||||
},
|
||||
"sections": {
|
||||
"message": "الأقسام"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "رجوع للإدارة"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها."
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "ممكّن"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "أدخل اسمًا"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "حفظ"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org."
|
||||
"message": "إدارة الأنماط المثبتة",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
|
||||
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه."
|
||||
"appliesSpecify": {
|
||||
"message": "تحديد",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "النمط محدّث."
|
||||
"installUpdate": {
|
||||
"message": "تثبيت التحديث",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "إزالة القسم",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Turn all styles off",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Undo (global)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "اكتمل التحديث."
|
||||
"message": "اكتمل التحديث.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "جارٍ البحث...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "الرمز",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Use smart indentation",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم.",
|
||||
"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"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Overwrite style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Append the imported style to current style",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Press a hotkey",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Replace all",
|
||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Goto line (or 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"
|
||||
},
|
||||
"issuesHelp": {
|
||||
"message": "The issues found by <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> with these rules enabled:",
|
||||
"description": "Help popup message for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "No",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Undo",
|
||||
"description": "Button label"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Keymap",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Use tabs with smart indentation",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Replace with",
|
||||
"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": "Only enabled styles",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "تعديل",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Theme",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Type a command name",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"description": {
|
||||
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى.",
|
||||
"description": "Extension description"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,566 +0,0 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Писане на нов стил"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Добавяне на стил"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Добавяне"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Приложимо за: $applies$",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "и още"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "Адреси на домейна"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела."
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Приложимо за"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "Адреси, съвпадащи с регулярен израз"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Премахване"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Уточняване"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Всичко"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "Адрес"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "Адреси, започващи с"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Прилагане на всички обновления"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Резервни копия"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Изнасяне на стилове"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Проверка на всички стилове за обновления"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Повторна проверка"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Проверка за обновления"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Проверяване..."
|
||||
},
|
||||
"cm_autocompleteOnTyping": {
|
||||
"message": "Автоматично завършване при въвеждане"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Подпрозорци с умен отстъп"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Клавиши"
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Пренасяне"
|
||||
},
|
||||
"cm_matchHighlight": {
|
||||
"message": "Осветяване"
|
||||
},
|
||||
"cm_matchHighlightSelection": {
|
||||
"message": "Само избраното"
|
||||
},
|
||||
"cm_matchHighlightToken": {
|
||||
"message": "Низа под показалеца"
|
||||
},
|
||||
"cm_resizeGripHint": {
|
||||
"message": "Щракнете два пъти за възстановяване/увеличаване на височината"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Умен отстъп"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Табулация"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Тема"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Отказ"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Изтриване"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Не"
|
||||
},
|
||||
"confirmOK": {
|
||||
"message": "Добре"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Запазване"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Спиране"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Да"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "по подразбиране"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Сигурни ли сте, че искате да изтриете стила?"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Изтриване"
|
||||
},
|
||||
"description": {
|
||||
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове."
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Изключване на всички стилове"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Изключване"
|
||||
},
|
||||
"dragDropMessage": {
|
||||
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете."
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Изтриване"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Отиване на ред"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Редактиране на стила"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Редактиране"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Редактиране на стила $stylename$",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Включване"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Изнасяне"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Добавяне"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Изключено"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Включено"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "Хронология"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Помощ"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Въведете име"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Натиснете клавиш"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Прибавяне към стила"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Прибавяне на внесения стил към текущия"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Внасяне"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Презаписване на стила"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Презаписване на съдържанието на текщия стил с това от внесения"
|
||||
},
|
||||
"importReportLegendAdded": {
|
||||
"message": "добавени"
|
||||
},
|
||||
"importReportLegendIdentical": {
|
||||
"message": "пропуснати еднакви"
|
||||
},
|
||||
"importReportLegendInvalid": {
|
||||
"message": "пропуснати невалидни"
|
||||
},
|
||||
"importReportLegendUpdatedBoth": {
|
||||
"message": "с обновени код и метаданни"
|
||||
},
|
||||
"importReportLegendUpdatedCode": {
|
||||
"message": "с обновен код"
|
||||
},
|
||||
"importReportLegendUpdatedMeta": {
|
||||
"message": "с обновени метаданни"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Внасянето на стилове завърши"
|
||||
},
|
||||
"importReportUnchanged": {
|
||||
"message": "Нищо не беше променено."
|
||||
},
|
||||
"importReportUndone": {
|
||||
"message": "върнати стила"
|
||||
},
|
||||
"importReportUndoneTitle": {
|
||||
"message": "Внасянето беше отменено"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Инсталиране на обновлението"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Помощ"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Вземете стилове"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Проблеми"
|
||||
},
|
||||
"linterIssuesHelp": {
|
||||
"message": "Проблеми, намерени от $link$ при следните правила:",
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageFavicons": {
|
||||
"message": "Иконки на приложимите сайтовете"
|
||||
},
|
||||
"manageFaviconsGray": {
|
||||
"message": "Сиви"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Филтри"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Инсталирани стилове"
|
||||
},
|
||||
"manageMaxTargets": {
|
||||
"message": "Брой на видимите приложими адреси"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "Нов интерфейс за управление"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Само включените стилове"
|
||||
},
|
||||
"manageOnlyLocal": {
|
||||
"message": "Само местно създадените стилове"
|
||||
},
|
||||
"manageOnlyLocalTooltip": {
|
||||
"message": "(стиловете, които не са инсталирани през userstyles.org)"
|
||||
},
|
||||
"manageOnlyUpdates": {
|
||||
"message": "Само стилове с обновления или проблеми"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Стилове"
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Брой на активните стилове"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Няма инсталирани стилове за сайта."
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Управление"
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Настройки"
|
||||
},
|
||||
"openStylesManager": {
|
||||
"message": "Управление на стиловете"
|
||||
},
|
||||
"optionsActions": {
|
||||
"message": "Действия"
|
||||
},
|
||||
"optionsAdvanced": {
|
||||
"message": "Разширени"
|
||||
},
|
||||
"optionsAdvancedContextDelete": {
|
||||
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора"
|
||||
},
|
||||
"optionsAdvancedExposeIframes": {
|
||||
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]"
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Цвят на фона, когато е изключено"
|
||||
},
|
||||
"optionsBadgeNormal": {
|
||||
"message": "Цвят на фона"
|
||||
},
|
||||
"optionsCheck": {
|
||||
"message": "Обновяване на стиловете"
|
||||
},
|
||||
"optionsCheckUpdate": {
|
||||
"message": "Проверка и инсталиране на наличните обновления"
|
||||
},
|
||||
"optionsCustomizeBadge": {
|
||||
"message": "Значка на иконката на лентата"
|
||||
},
|
||||
"optionsCustomizeIcon": {
|
||||
"message": "Иконка на лентата със сечива"
|
||||
},
|
||||
"optionsCustomizePopup": {
|
||||
"message": "Падащ прозорец"
|
||||
},
|
||||
"optionsCustomizeUpdate": {
|
||||
"message": "Обновления"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Настройки"
|
||||
},
|
||||
"optionsIconDark": {
|
||||
"message": "Тъмни теми"
|
||||
},
|
||||
"optionsIconLight": {
|
||||
"message": "Светли теми"
|
||||
},
|
||||
"optionsOpen": {
|
||||
"message": "Отваряне"
|
||||
},
|
||||
"optionsOpenManager": {
|
||||
"message": "Управление на стиловете"
|
||||
},
|
||||
"optionsPopupWidth": {
|
||||
"message": "Ширина на падащия прозорец (в пиксели)"
|
||||
},
|
||||
"optionsReset": {
|
||||
"message": "Зануляване на настройки на първоначалните стойности"
|
||||
},
|
||||
"optionsResetButton": {
|
||||
"message": "Зануляване на настройките"
|
||||
},
|
||||
"optionsSubheading": {
|
||||
"message": "Още настройки"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "Адрес"
|
||||
},
|
||||
"optionsUpdateImportNote": {
|
||||
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални."
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "Стилове преди командите"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Брой на активните стилове за текущия сайт"
|
||||
},
|
||||
"replace": {
|
||||
"message": "Заместване"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Заместване на всички"
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Заместване с"
|
||||
},
|
||||
"retrieveBckp": {
|
||||
"message": "Внасяне на стилове"
|
||||
},
|
||||
"search": {
|
||||
"message": "Търсене"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Добавяне на друг отдел"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Код"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Премахване на отдела"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Отдели"
|
||||
},
|
||||
"shortcuts": {
|
||||
"message": "Клавишни комбинации"
|
||||
},
|
||||
"shortcutsNote": {
|
||||
"message": "Задаване на клавишни комбинации"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Регулярният израз не е правилен."
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Разкрасяване"
|
||||
},
|
||||
"styleBeautifyIndentConditional": {
|
||||
"message": "Отстъп на @media, @supports"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Назад към стиловете"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Направили сте промени по стила без да ги запазите."
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Включено"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Поставете кода във формат на Мозила"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Да се инсталира ли '$stylename$'?",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Въведете име"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Формат на Мозила"
|
||||
},
|
||||
"styleNotAppliedRegexpProblemTooltip": {
|
||||
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази"
|
||||
},
|
||||
"styleRegexpInvalidExplanation": {
|
||||
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани."
|
||||
},
|
||||
"styleRegexpPartialExplanation": {
|
||||
"message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект)."
|
||||
},
|
||||
"styleRegexpProblemTooltip": {
|
||||
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази"
|
||||
},
|
||||
"styleRegexpTestFull": {
|
||||
"message": "Съвпадащи подпрозорци"
|
||||
},
|
||||
"styleRegexpTestInvalid": {
|
||||
"message": "Неправилните регулярни изрази са пропуснати"
|
||||
},
|
||||
"styleRegexpTestNone": {
|
||||
"message": "Няма съвпадащи подпрозорци"
|
||||
},
|
||||
"styleRegexpTestPartial": {
|
||||
"message": "Не съвпада напълно, затова е пропуснато"
|
||||
},
|
||||
"styleRegexpTestTitle": {
|
||||
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Запазване"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)"
|
||||
},
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Стил във формат на Мозила"
|
||||
},
|
||||
"styleUpdate": {
|
||||
"message": "Сигурни ли сте, че искате да обновите '$stylename$'?",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Разширението не работи на такива страници."
|
||||
},
|
||||
"stylusUnavailableForURLdetails": {
|
||||
"message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен."
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Превключване на стила"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Отмяна"
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Отмяна във всички отдели"
|
||||
},
|
||||
"unreachableContentScript": {
|
||||
"message": "Няма връзка със страницата. Презаредете подпрозореца."
|
||||
},
|
||||
"unreachableFileHint": {
|
||||
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions."
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Няма намерени обновления."
|
||||
},
|
||||
"updateAllCheckSucceededSomeEdited": {
|
||||
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани)."
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Неуспешно обновяване: сървърът отговори с код $code$.",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Неуспешно обновяване: няма връзка със сървъра."
|
||||
},
|
||||
"updateCheckHistory": {
|
||||
"message": "Хронология на проверките"
|
||||
},
|
||||
"updateCheckManualUpdateForce": {
|
||||
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)"
|
||||
},
|
||||
"updateCheckManualUpdateHint": {
|
||||
"message": "Принудителното обновяване ще презапише местните редакции."
|
||||
},
|
||||
"updateCheckSkippedLocallyEdited": {
|
||||
"message": "Стилът е бил местно редактиран."
|
||||
},
|
||||
"updateCheckSkippedMaybeLocallyEdited": {
|
||||
"message": "Стилът може да е бил местно редактиран."
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Стилът е обновен."
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Обновяването е завършено."
|
||||
},
|
||||
"updatesCurrentlyInstalled": {
|
||||
"message": "Инсталирани обновления:"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Писане на стил за: "
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "този адрес"
|
||||
}
|
||||
}
|
|
@ -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,417 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Uusi Tyyli"
|
||||
"appliesToEverything": {
|
||||
"message": "Kaikki",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Lisää Tyyli"
|
||||
"defaultTheme": {
|
||||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"issues": {
|
||||
"message": "Issues",
|
||||
"description": "Label for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Tab size",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Aktivoi",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Syötä nimi.",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL ositteita domainilla",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Hae päivityksiä",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Append to style",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "All styles are up to date.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Paste the Mozilla-format code",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Apu",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Yes",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"findStylesForSite": {
|
||||
"message": "Hae lisää tyylejä tälle sivustolle.",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Asennetut Tyylit",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Beautify",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Aktivoitu",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Lisää uusi osio",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Tallenna",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Stop",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "this URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Lisää"
|
||||
"message": "Lisää",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL ositteet jotka vastaavat regexpiä",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Search contents",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Poista Käytöstä",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Show number of styles active for the current site on the toolbar button",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Show active style count",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Word wrap",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Takaisin hallintapaneeliin",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Import",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Filters",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Apply all updates",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Oletko varma että haluat poistaa tämän tyylin?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Cancel"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp ei kelpaa.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Options",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Kooskee: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "ja lisää"
|
||||
"styleUpdate": {
|
||||
"message": "Are you sure you want to update '$stylename$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL ositteita domainilla"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee."
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Koskee"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL ositteet jotka vastaavat regexpiä"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Poista"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Tarkenna"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Kaikki"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL osoitteet jotka alkavat"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Tarkista kaikki tyylit päivityksien varalta"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Hae päivityksiä"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Tarkistetaan..."
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Poista"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Tallenna"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Oletko varma että haluat poistaa tämän tyylin?"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Poista"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Poista Käytöstä"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Poista"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Muokkaa Tyyliä"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Muokkaa"
|
||||
"styleSectionsTitle": {
|
||||
"message": "Osiot",
|
||||
"description": "Title for the style sections section"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Muokkaa Tyyliä $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Aktivoi"
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Tyyli on ajan tasalla.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Lisää"
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL osoitteet jotka alkavat",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Aktivoitu"
|
||||
"searchRegexp": {
|
||||
"message": "Use /re/ syntax for regexp search",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Apu"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Asenna päivitys"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Asennetut Tyylit"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Tyylikäs"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Ei asennettuja tyylejä tällä sivustolla."
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Hallitse asennettuja tyylejä"
|
||||
"importReplaceTooltip": {
|
||||
"message": "Discard contents of current style and overwrite it with the imported style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "List styles before commands in the toolbar button menu"
|
||||
"message": "List styles before commands in the toolbar button menu",
|
||||
"description": "Label for the checkbox controlling section order in the toolbar button menu."
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Show number of styles active for the current site on the toolbar button"
|
||||
"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.",
|
||||
"description": "Help text for sections"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Lisää uusi osio"
|
||||
"noStylesForSite": {
|
||||
"message": "Ei asennettuja tyylejä tällä sivustolla.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Koodi"
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "ja lisää",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Poista osio"
|
||||
"appliesRemove": {
|
||||
"message": "Poista",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Osiot"
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Style in Mozilla format",
|
||||
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp ei kelpaa."
|
||||
"manageTitle": {
|
||||
"message": "Tyylikäs",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Takaisin hallintapaneeliin"
|
||||
"writeStyleFor": {
|
||||
"message": "Write style for: ",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta."
|
||||
"replace": {
|
||||
"message": "Replace",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Aktivoitu"
|
||||
"appliesLabel": {
|
||||
"message": "Koskee",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Syötä nimi"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Tallenna"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin."
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "All styles are up to date."
|
||||
"openManage": {
|
||||
"message": "Hallitse asennettuja tyylejä",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
|
||||
"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",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen."
|
||||
"appliesSpecify": {
|
||||
"message": "Tarkenna",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Tyyli on ajan tasalla."
|
||||
"installUpdate": {
|
||||
"message": "Asenna päivitys",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Poista osio",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Turn all styles off",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Undo (global)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Päivitys suoritettu."
|
||||
"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"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Use smart indentation",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "URL",
|
||||
"description": "Option to make the style apply to the entered string as a URL"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Lisää Tyyli",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Overwrite style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Append the imported style to current style",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Press a hotkey",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Replace all",
|
||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Goto line (or line:col)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Tarkista kaikki tyylit päivityksien varalta",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"issuesHelp": {
|
||||
"message": "The issues found by <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> with these rules enabled:",
|
||||
"description": "Help popup message for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "No",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Undo",
|
||||
"description": "Button label"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Keymap",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Use tabs with smart indentation",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Replace with",
|
||||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Only enabled styles",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Muokkaa",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Theme",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Type a command name",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"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,421 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Упиши нови стил"
|
||||
"appliesToEverything": {
|
||||
"message": "Све",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Додај стил"
|
||||
"defaultTheme": {
|
||||
"message": "подразумевано",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Извези",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"issues": {
|
||||
"message": "Проблеми",
|
||||
"description": "Label for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Величина картице",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Омогући",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Унесите назив",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "УРЛ адресе на домену",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Проверите ажурирање",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Додај стилу",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Сви стилови су ажурирани.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Налепи код у Mozilla формату",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Помоћ",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"search": {
|
||||
"message": "Претражи",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Да",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"findStylesForSite": {
|
||||
"message": "Пронађи још стилова за овај сајт.",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Инсталирани стилови",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": " Улепшај",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Омогућено",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Додај нови одељак",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Сачувај",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Заустави",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "ову УРЛ адресу",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Додај"
|
||||
"message": "Додај",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "УРЛ адресе које одговарају регуларном изразу",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Инсталирати '$stylename$' у Stylus?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Преузмите стилове са userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Помоћ</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Претражи садржај",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Онемогући",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Прикажи број активних стилова",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Преламање текста",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Назад на управљање",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Направили сте измене овог стила које нисте сачували.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Увези",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Ажурирање није успело - сервер није доступан.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Филтери",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Примени сва ажурирања",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Да ли сте сигурни да желите да избришете овај стил?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Cancel"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Регуларни израз је неисправан.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Опције",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Примењује се на: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "и још"
|
||||
"styleUpdate": {
|
||||
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "УРЛ адресе на домену"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује."
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Примењује се на"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "УРЛ адресе које одговарају регуларном изразу"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Уклони"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Детаљније"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Све"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "УРЛ"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "УРЛ адресе које почињу са"
|
||||
},
|
||||
"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": "Тема"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Избриши"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Не"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Сачувај"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Заустави"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Да"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "подразумевано"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Да ли сте сигурни да желите да избришете овај стил?"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Избриши"
|
||||
},
|
||||
"description": {
|
||||
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове."
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Искључи све стилове"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Онемогући"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Избриши"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Иди на ред (или line:col)"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Уреди стил"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Уреди"
|
||||
"styleSectionsTitle": {
|
||||
"message": "Одељци",
|
||||
"description": "Title for the style sections section"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Уреди стил $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Омогући"
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Стил је ажуриран.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Извези"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Додај"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Омогућено"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Помоћ"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Укуцај име команде"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Притисни пречицу"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Додај стилу"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Додај увезени стил тренутном стилу"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Увези"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Упиши преко стила"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Инсталирај ажурирање"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Помоћ"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Преузмите стилове"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Проблеми"
|
||||
},
|
||||
"linterIssuesHelp": {
|
||||
"message": "Проблем пронађен од стране $link$:",
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Филтери"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Инсталирани стилови"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Само омогућени стилови"
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Прикажи број активних стилова"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Нема инсталираних стилова за овај сајт."
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Управљај инсталираним стиловима"
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Опције"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Опције"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "УРЛ"
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "Излистај стилове пре команди у менију дугмета на алатној траци"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци"
|
||||
},
|
||||
"replace": {
|
||||
"message": "Замени"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Замени све"
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Замени са"
|
||||
},
|
||||
"search": {
|
||||
"message": "Претражи"
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "УРЛ адресе које почињу са",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Користи /re/ синтаксу за претрагу регуларним изразом"
|
||||
"message": "Користи /re/ синтаксу за претрагу регуларним изразом",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Додај нови одељак"
|
||||
"importReplaceTooltip": {
|
||||
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Код"
|
||||
"popupStylesFirst": {
|
||||
"message": "Излистај стилове пре команди у менију дугмета на алатној траци",
|
||||
"description": "Label for the checkbox controlling section order in the toolbar button menu."
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Уклони одељак"
|
||||
"sectionHelp": {
|
||||
"message": "Одељци вам омогућавају да дефинишете различите делове кода који се примењују на раличите скупове УРЛ-ова у истом стилу. На пример, један исти стил може променити почетну страницу једног сајта на један начин а остатак сајта на други начин.",
|
||||
"description": "Help text for sections"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Одељци"
|
||||
"noStylesForSite": {
|
||||
"message": "Нема инсталираних стилова за овај сајт.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Регуларни израз је неисправан."
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "и још",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Улепшај"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Назад на управљање"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Направили сте измене овог стила које нисте сачували."
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Омогућено"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Налепи код у Mozilla формату"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Инсталирати '$stylename$' у Stylus?",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Унесите назив"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla формат"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Сачувај"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org."
|
||||
"appliesRemove": {
|
||||
"message": "Уклони",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Стил у Mozilla формату"
|
||||
"message": "Стил у Mozilla формату",
|
||||
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
|
||||
},
|
||||
"styleUpdate": {
|
||||
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
"manageTitle": {
|
||||
"message": "Stylus",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus не ради на страницама као што је ова."
|
||||
"writeStyleFor": {
|
||||
"message": "Упиши стил за:",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Опозови"
|
||||
"replace": {
|
||||
"message": "Замени",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Опозови (свеобухватно)"
|
||||
"appliesLabel": {
|
||||
"message": "Примењује се на",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Сви стилови су ажурирани."
|
||||
"openManage": {
|
||||
"message": "Управљај инсталираним стиловима",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
|
||||
"message": "Ажурирање није успело - сервер је одговорио кодом $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Ажурирање није успело: сервер није доступан."
|
||||
"appliesSpecify": {
|
||||
"message": "Детаљније",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Стил је ажуриран."
|
||||
"installUpdate": {
|
||||
"message": "Инсталирај ажурирање",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla формат",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus не ради на страницама као што је ова.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Уклони одељак",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Искључи све стилове",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Опозови (свеобухватно)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Ажурирање је комплетирано."
|
||||
"message": "Ажурирање је комплетирано.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Упиши стил за:"
|
||||
"checkingForUpdate": {
|
||||
"message": "Проверавање...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "ову УРЛ адресу"
|
||||
"sectionCode": {
|
||||
"message": "Код",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Користи паметно увлачење редова",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Уреди стил",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "УРЛ",
|
||||
"description": "Option to make the style apply to the entered string as a URL"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"issuesHelp": {
|
||||
"message": "Проблем пронађен од стране <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> са овим омогућеним правилима:",
|
||||
"description": "Help popup message for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"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
421
_locales/sv_SE/messages.json
Normal file
421
_locales/sv_SE/messages.json
Normal file
|
@ -0,0 +1,421 @@
|
|||
{
|
||||
"appliesToEverything": {
|
||||
"message": "Allt",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"issues": {
|
||||
"message": "Issues",
|
||||
"description": "Label for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Flikstorlek",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Aktivera",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Ange ett namn.",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL:er på domänen",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Leta efter uppdatering",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Append to style",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Alla stilar är fullt uppdaterade.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Paste the Mozilla-format code",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Hjälp",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Yes",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"findStylesForSite": {
|
||||
"message": "Hitta fler stilar för denna sida.",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Installerade Stilar",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Beautify",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Aktiverad",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla-formatet av koden fungerar i Stylish till Firefox samt vid uppladdandet till userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Lägg till ytterligare en sektion",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Spara",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Stop",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "denna URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Lägg till",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL:er som matchar regexp:en",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Installera '$stylename$' in i Stylus?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Search contents",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Inaktivera",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Visa antalet aktiva stilar för den nuvarande sidan på verktygsfältsikonen",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Visa antalet aktiva stilar",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Radbrytning",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Återgå till hantera",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Du har gjort ändringar i denna stil utan att spara.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Import",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Uppdateringen misslyckades - server onåbar.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Filter",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Verkställ alla uppdateringar",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Är du säker på att du vill ta bort denna stil?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Cancel"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp:en är ogiltig.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Alternativ",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Gäller för: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleUpdate": {
|
||||
"message": "Är du säker på att du vill uppdatera '$stylename$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleSectionsTitle": {
|
||||
"message": "Sektioner",
|
||||
"description": "Title for the style sections section"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Ändra i Stil $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Stilen är fullt uppdaterad.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL:er som börjar på",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Use /re/ syntax for regexp search",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Discard contents of current style and overwrite it with the imported style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "Lista stilar före kommandon i verktygsfältets knappmeny",
|
||||
"description": "Label for the checkbox controlling section order in the toolbar button menu."
|
||||
},
|
||||
"sectionHelp": {
|
||||
"message": "Sektioner låter dig definiera olika sorters kod som påverkar olika URL:er i samma stil. Till exempel, en stil kan ändra en viss hemsida på ett sätt, samtidigt som den ändrar andra delar på en helt annan sida.",
|
||||
"description": "Help text for sections"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Inga stilar installerade för denna sida.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "och mer",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Ta bort",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Style in Mozilla format",
|
||||
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Stylus",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Skapa stil för:",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"replace": {
|
||||
"message": "Replace",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Gäller för",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Hantera installerade stilar",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Uppdateringen misslyckades - servern svarade med kod $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Specificera",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Installera uppdatering",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus fungerar inte på sidor som dessa.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Ta bort sektion",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Stäng av alla stilar",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Undo (global)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Uppdatering slutförd.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Letar...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Kod",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Använd smart indrag",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Använd 'Gäller för' alternativet för att begränsa vilka URL:er koden i denna sektion gäller för.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Ändra i Stil",
|
||||
"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": "Lägg till stil",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Overwrite style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Ett fel inträffades vid hanteringen av Stylus-databasen. Skulle du vilja besöka en sida med eventuella lösningar?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Append the imported style to current style",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Press a hotkey",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Replace all",
|
||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Gå till rad (eller rad:kol)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Leta igenom alla stilar efter uppdateringar",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"issuesHelp": {
|
||||
"message": "The issues found by <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> with these rules enabled:",
|
||||
"description": "Help popup message for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "No",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Undo",
|
||||
"description": "Button label"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Nyckelkarta",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Använd flikar med smart indrag",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Replace with",
|
||||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Ta bort",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"addStyleLabel": {
|
||||
"message": "Skriv ny stil",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Endast aktiverade stilar",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Ändra",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Theme",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Type a command name",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"description": {
|
||||
"message": "Style:a om webben med Stylus, en användarstils-hanterare. Stylus låter dig enkelt installera teman och skal för många populära sidor.",
|
||||
"description": "Extension description"
|
||||
}
|
||||
}
|
|
@ -1,67 +1,417 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "క్రొత్త స్టైల్ వ్రాయండి"
|
||||
"appliesToEverything": {
|
||||
"message": "అన్నిటికీ",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"issues": {
|
||||
"message": "Issues",
|
||||
"description": "Label for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Tab size",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "చేతనించు",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Enter a name.",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URLs on the domain",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Check for update",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Append to style",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "All styles are up to date.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Paste the Mozilla-format code",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "సహాయం",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Yes",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"findStylesForSite": {
|
||||
"message": "Find more styles for this site.",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "స్థాపిత శైలులు",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Beautify",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Enabled",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "The Mozilla format of the code can be used with Stylish for Firefox and can be submitted to userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Add another section",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "భద్రపరచు",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Stop",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "this URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "చేర్చు"
|
||||
"message": "చేర్చు",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URLs matching the regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Install '$stylename$' into Stylus?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Search contents",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "అచేతనించు",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Show number of styles active for the current site on the toolbar button",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Show active style count",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Word wrap",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Back to manage",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "You've made changes to this style without saving.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Import",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Update failed - server unreachable.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Filters",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Apply all updates",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Cancel"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp is invalid.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Options",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "వేటికి వర్తిస్తుంది; $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleUpdate": {
|
||||
"message": "Are you sure you want to update '$stylename$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleSectionsTitle": {
|
||||
"message": "విభాగాలు",
|
||||
"description": "Title for the style sections section"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Edit Style $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Style is up to date.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URLs starting with",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Use /re/ syntax for regexp search",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Discard contents of current style and overwrite it with the imported style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "List styles before commands in the toolbar button menu",
|
||||
"description": "Label for the checkbox controlling section order in the toolbar button menu."
|
||||
},
|
||||
"sectionHelp": {
|
||||
"message": "Sections let you define different pieces of code to apply to different sets of URLs in the same style. For example, a single style could change the homepage of a site one way, while changing the rest of a site another way.",
|
||||
"description": "Help text for sections"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "No styles installed for this site.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "ఇంకా మరిన్ని"
|
||||
"message": "ఇంకా మరిన్ని",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "తొలగించు"
|
||||
"message": "తొలగించు",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "అన్నిటికీ"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "తొలగించు"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "భద్రపరచు"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "తొలగించు"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "అచేతనించు"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "తొలగించు"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "మార్చు"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "చేతనించు"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "చేర్చు"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "సహాయం"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "స్థాపిత శైలులు"
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Style in Mozilla format",
|
||||
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "స్టైలిష్"
|
||||
"message": "స్టైలిష్",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"sections": {
|
||||
"message": "విభాగాలు"
|
||||
"writeStyleFor": {
|
||||
"message": "Write style for: ",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "భద్రపరచు"
|
||||
"replace": {
|
||||
"message": "Replace",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Applies to",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Manage installed styles",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Update failed - server responded with code $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Specify",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Install update",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Remove section",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Turn all styles off",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Undo (global)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Update completed.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Checking...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Code",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Use smart indentation",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Use the 'Applies to' controls to limit what URLs the code in this section applies to.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Edit Style",
|
||||
"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": "Add Style",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Overwrite style",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Append the imported style to current style",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Press a hotkey",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Replace all",
|
||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Goto line (or line:col)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Check all styles for updates",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"issuesHelp": {
|
||||
"message": "The issues found by <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> with these rules enabled:",
|
||||
"description": "Help popup message for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "No",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Undo",
|
||||
"description": "Button label"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Keymap",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Use tabs with smart indentation",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Replace with",
|
||||
"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": "Only enabled styles",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "మార్చు",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Theme",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Type a command name",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"description": {
|
||||
"message": "Restyle the web with Stylus, a user styles manager. Stylus lets you easily install themes and skins for many popular sites.",
|
||||
"description": "Extension description"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
336
apply.js
Normal file
336
apply.js
Normal file
|
@ -0,0 +1,336 @@
|
|||
// Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/
|
||||
// like destructring, classes, defaults, spread, calculated key names
|
||||
/* eslint no-var: 0 */
|
||||
'use strict';
|
||||
|
||||
var ID_PREFIX = 'stylus-';
|
||||
var ROOT = document.documentElement;
|
||||
var isOwnPage = location.href.startsWith('chrome-extension:');
|
||||
var disableAll = false;
|
||||
var exposeIframes = false;
|
||||
var styleElements = new Map();
|
||||
var disabledElements = new Map();
|
||||
var retiredStyleTimers = new Map();
|
||||
var docRewriteObserver;
|
||||
|
||||
requestStyles();
|
||||
chrome.runtime.onMessage.addListener(applyOnMessage);
|
||||
|
||||
if (!isOwnPage) {
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
}
|
||||
|
||||
function requestStyles(options, callback = applyStyles) {
|
||||
var matchUrl = location.href;
|
||||
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||
// 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',
|
||||
matchUrl,
|
||||
enabled: true,
|
||||
asHash: true,
|
||||
}, options);
|
||||
// On own pages we request the styles directly to minimize delay and flicker
|
||||
if (typeof getStylesSafe !== 'undefined') {
|
||||
getStylesSafe(request).then(callback);
|
||||
} else {
|
||||
chrome.runtime.sendMessage(request, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applyOnMessage(request, sender, sendResponse) {
|
||||
if (request.styles == 'DIY') {
|
||||
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
||||
// which is faster because IPC messaging JSON-ifies everything internally
|
||||
requestStyles({}, styles => {
|
||||
request.styles = styles;
|
||||
applyOnMessage(request);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
|
||||
case 'styleDeleted':
|
||||
removeStyle(request);
|
||||
break;
|
||||
|
||||
case 'styleUpdated':
|
||||
if (request.codeIsUpdated === false) {
|
||||
applyStyleState(request.style);
|
||||
break;
|
||||
}
|
||||
if (request.style.enabled) {
|
||||
removeStyle({id: request.style.id, retire: true});
|
||||
requestStyles({id: request.style.id});
|
||||
} else {
|
||||
removeStyle(request.style);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleAdded':
|
||||
if (request.style.enabled) {
|
||||
requestStyles({id: request.style.id});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleApply':
|
||||
applyStyles(request.styles);
|
||||
break;
|
||||
|
||||
case 'styleReplaceAll':
|
||||
replaceAll(request.styles);
|
||||
break;
|
||||
|
||||
case 'prefChanged':
|
||||
if ('disableAll' in request.prefs) {
|
||||
doDisableAll(request.prefs.disableAll);
|
||||
}
|
||||
if ('exposeIframes' in request.prefs) {
|
||||
doExposeIframes(request.prefs.exposeIframes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
sendResponse(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function doDisableAll(disable = disableAll) {
|
||||
if (!disable === !disableAll) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
if (inDoc) {
|
||||
disabledElements.set(id, inDoc);
|
||||
inDoc.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeStyle({id, retire = false}) {
|
||||
const el = document.getElementById(ID_PREFIX + id);
|
||||
if (el) {
|
||||
if (retire) {
|
||||
// to avoid page flicker when the style is updated
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
styleElements.delete(ID_PREFIX + id);
|
||||
disabledElements.delete(id);
|
||||
retiredStyleTimers.delete(id);
|
||||
}
|
||||
|
||||
|
||||
function applyStyles(styles) {
|
||||
if (!styles) {
|
||||
// Chrome is starting up
|
||||
requestStyles();
|
||||
return;
|
||||
}
|
||||
if ('disableAll' in styles) {
|
||||
doDisableAll(styles.disableAll);
|
||||
delete styles.disableAll;
|
||||
}
|
||||
if ('exposeIframes' in styles) {
|
||||
doExposeIframes(styles.exposeIframes);
|
||||
delete styles.exposeIframes;
|
||||
}
|
||||
if (document.head
|
||||
&& document.head.firstChild
|
||||
&& document.head.firstChild.id == 'xml-viewer-style') {
|
||||
// when site response is application/xml Chrome displays our style elements
|
||||
// under document.documentElement as plain text so we need to move them into HEAD
|
||||
// which is already autogenerated at this moment
|
||||
ROOT = document.head;
|
||||
}
|
||||
for (const id in styles) {
|
||||
applySections(id, styles[id]);
|
||||
}
|
||||
initDocRewriteObserver();
|
||||
if (retiredStyleTimers.size) {
|
||||
setTimeout(() => {
|
||||
for (const [id, timer] of retiredStyleTimers.entries()) {
|
||||
removeStyle({id});
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applySections(styleId, sections) {
|
||||
let el = document.getElementById(ID_PREFIX + styleId);
|
||||
if (el) {
|
||||
return;
|
||||
}
|
||||
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: ID_PREFIX + styleId,
|
||||
className: 'stylus',
|
||||
type: 'text/css',
|
||||
textContent: sections.map(section => section.code).join('\n'),
|
||||
});
|
||||
addStyleElement(el);
|
||||
styleElements.set(el.id, el);
|
||||
disabledElements.delete(styleId);
|
||||
}
|
||||
|
||||
|
||||
function addStyleElement(el) {
|
||||
if (ROOT && !document.getElementById(el.id)) {
|
||||
ROOT.appendChild(el);
|
||||
el.disabled = disableAll;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 initDocRewriteObserver() {
|
||||
if (isOwnPage || docRewriteObserver || !styleElements.size) {
|
||||
return;
|
||||
}
|
||||
// re-add styles if we detect documentElement being recreated
|
||||
const reinjectStyles = () => {
|
||||
if (!styleElements) {
|
||||
return orphanCheck && orphanCheck();
|
||||
}
|
||||
ROOT = document.documentElement;
|
||||
for (const el of styleElements.values()) {
|
||||
addStyleElement(document.importNode(el, true));
|
||||
}
|
||||
};
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function orphanCheck() {
|
||||
const port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach the mutation observer
|
||||
if (docRewriteObserver) {
|
||||
docRewriteObserver.disconnect();
|
||||
}
|
||||
// we can detach event listeners
|
||||
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
// we can destroy our globals in this context to free up memory
|
||||
[ // functions
|
||||
'addStyleElement',
|
||||
'applyOnMessage',
|
||||
'applySections',
|
||||
'applyStyles',
|
||||
'applyStyleState',
|
||||
'doDisableAll',
|
||||
'initDocRewriteObserver',
|
||||
'orphanCheck',
|
||||
'removeStyle',
|
||||
'replaceAll',
|
||||
'requestStyles',
|
||||
// variables
|
||||
'ROOT',
|
||||
'disabledElements',
|
||||
'retiredStyleTimers',
|
||||
'styleElements',
|
||||
'docRewriteObserver',
|
||||
].forEach(fn => (window[fn] = null));
|
||||
}
|
287
background.js
Normal file
287
background.js
Normal file
|
@ -0,0 +1,287 @@
|
|||
/* global dbExec, getStyles, saveStyle, schedule, download */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var browserCommands, contextMenus;
|
||||
|
||||
// *************************************************************************
|
||||
// preload the DB and report errors
|
||||
dbExec().catch((...args) => {
|
||||
args.forEach(arg => 'message' in arg && console.error(arg.message));
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
// register all listeners
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(data =>
|
||||
webNavigationListener(null, data));
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener(data =>
|
||||
webNavigationListener('styleApply', data));
|
||||
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||
webNavigationListener('styleReplaceAll', data));
|
||||
|
||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||
webNavigationListener('styleReplaceAll', data));
|
||||
|
||||
chrome.tabs.onAttached.addListener((tabId, data) => {
|
||||
// When an edit page gets attached or detached, remember its state
|
||||
// so we can do the same to the next one to open.
|
||||
chrome.tabs.get(tabId, tab => {
|
||||
if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) {
|
||||
chrome.windows.get(tab.windowId, {populate: true}, win => {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
prefs.set('openEditInWindow', win.tabs.length == 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
||||
contextMenus[info.menuItemId].click(info, tab));
|
||||
|
||||
if ('commands' in chrome) {
|
||||
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
|
||||
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
{
|
||||
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) {
|
||||
setTimeout(openURL, 100, {
|
||||
url: `http://add0n.com/stylus.html?version=${manifest.version}&type=install`
|
||||
});
|
||||
}
|
||||
// reset L10N cache on UI language change or update
|
||||
const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {};
|
||||
const UIlang = chrome.i18n.getUILanguage();
|
||||
if (reason == 'update' || browserUIlanguage != UIlang) {
|
||||
localStorage.L10N = JSON.stringify({
|
||||
browserUIlanguage: UIlang,
|
||||
});
|
||||
}
|
||||
};
|
||||
// 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'});
|
||||
},
|
||||
styleDisableAll(info) {
|
||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||
},
|
||||
};
|
||||
|
||||
// *************************************************************************
|
||||
// context menus
|
||||
contextMenus = Object.assign({
|
||||
'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,
|
||||
},
|
||||
}, prefs.get('editor.contextDelete') && {
|
||||
'editor.contextDelete': {
|
||||
title: 'editDeleteText',
|
||||
type: 'normal',
|
||||
contexts: ['editable'],
|
||||
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
||||
click: (info, tab) => {
|
||||
chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const createContextMenus = (ids = Object.keys(contextMenus)) => {
|
||||
for (const id of ids) {
|
||||
const item = Object.assign({id}, contextMenus[id]);
|
||||
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);
|
||||
}
|
||||
};
|
||||
createContextMenus();
|
||||
prefs.subscribe((id, checked) => {
|
||||
if (id == 'editor.contextDelete') {
|
||||
if (checked) {
|
||||
createContextMenus([id]);
|
||||
} else {
|
||||
chrome.contextMenus.remove(id, ignoreChromeError);
|
||||
}
|
||||
} else {
|
||||
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
|
||||
}
|
||||
}, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean'));
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
// [re]inject content scripts
|
||||
{
|
||||
const NTP = 'chrome://newtab/';
|
||||
const PING = {method: 'ping'};
|
||||
const ALL_URLS = '<all_urls>';
|
||||
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||
// expand * as .*?
|
||||
const wildcardAsRegExp = (s, flags) => new RegExp(
|
||||
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}) => {
|
||||
cs.matches.some(match => {
|
||||
if ((match == ALL_URLS || url.match(match))
|
||||
&& (!url.startsWith('chrome') || url == NTP)) {
|
||||
chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
chrome.tabs.query({}, tabs =>
|
||||
tabs.forEach(tab =>
|
||||
contentScripts.forEach(cs =>
|
||||
pingCS(cs, tab))));
|
||||
}
|
||||
|
||||
|
||||
// *************************************************************************
|
||||
|
||||
function webNavigationListener(method, {url, tabId, frameId}) {
|
||||
getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => {
|
||||
if (method && !url.startsWith('chrome:') && tabId >= 0) {
|
||||
chrome.tabs.sendMessage(tabId, {
|
||||
method,
|
||||
// ping own page so it retrieves the styles directly
|
||||
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
|
||||
}, {
|
||||
frameId
|
||||
});
|
||||
}
|
||||
// main page frame id is 0
|
||||
if (frameId == 0) {
|
||||
updateIcon({id: tabId, url}, styles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateIcon(tab, styles) {
|
||||
if (tab.id < 0) {
|
||||
return;
|
||||
}
|
||||
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) : '';
|
||||
chrome.browserAction.setIcon({
|
||||
tabId: tab.id,
|
||||
path: {
|
||||
// Material Design 2016 new size is 16px
|
||||
16: `images/icon/16${postfix}.png`,
|
||||
32: `images/icon/32${postfix}.png`,
|
||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||
19: `images/icon/19${postfix}.png`,
|
||||
38: `images/icon/38${postfix}.png`,
|
||||
// TODO: add Edge preferred sizes: 20, 25, 30, 40
|
||||
},
|
||||
}, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
return;
|
||||
}
|
||||
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
getTab(tab.id).then(() => {
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onRuntimeMessage(request, sender, sendResponse) {
|
||||
switch (request.method) {
|
||||
|
||||
case 'getStyles':
|
||||
getStyles(request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'saveStyle':
|
||||
saveStyle(request).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 'schedule':
|
||||
schedule.entry(request)
|
||||
.then(() => sendResponse(true))
|
||||
.catch(() => sendResponse(false));
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
}
|
||||
}
|
|
@ -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 +0,0 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global addAPI bgReady */// common.js
|
||||
/* global createWorker */// worker-util.js
|
||||
/* global prefs */
|
||||
/* 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';
|
||||
|
||||
//#region API
|
||||
|
||||
addAPI(/** @namespace API */ {
|
||||
|
||||
/** Temporary storage for data needed elsewhere e.g. in a content script */
|
||||
data: ((data = {}) => ({
|
||||
del: key => delete data[key],
|
||||
get: key => data[key],
|
||||
has: key => key in data,
|
||||
pop: key => {
|
||||
const val = data[key];
|
||||
delete data[key];
|
||||
return val;
|
||||
},
|
||||
set: (key, val) => {
|
||||
data[key] = val;
|
||||
},
|
||||
}))(),
|
||||
|
||||
styles: styleMan,
|
||||
sync: syncMan,
|
||||
updater: updateMan,
|
||||
usercss: usercssMan,
|
||||
uso: usoApi,
|
||||
usw: uswApi,
|
||||
colorScheme,
|
||||
/** @type {BackgroundWorker} */
|
||||
worker: createWorker({url: '/background/background-worker'}),
|
||||
|
||||
/** @returns {string} */
|
||||
getTabUrlPrefix() {
|
||||
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the editor or activates an existing tab
|
||||
* @param {{
|
||||
id?: number
|
||||
domain?: string
|
||||
'url-prefix'?: string
|
||||
}} params
|
||||
* @returns {Promise<chrome.tabs.Tab>}
|
||||
*/
|
||||
async openEditor(params) {
|
||||
const u = new URL(chrome.runtime.getURL('edit.html'));
|
||||
u.search = new URLSearchParams(params);
|
||||
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>} */
|
||||
async openManage({options = false, search, searchMode} = {}) {
|
||||
const setUrlParams = url => {
|
||||
const u = new URL(url);
|
||||
if (search) u.searchParams.set('search', search);
|
||||
if (searchMode) u.searchParams.set('searchMode', searchMode);
|
||||
if (options) u.hash = '#stylus-options';
|
||||
return u.href;
|
||||
};
|
||||
const base = chrome.runtime.getURL('manage.html');
|
||||
const url = setUrlParams(base);
|
||||
const tabs = await browser.tabs.query({url: base + '*'});
|
||||
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
|
||||
tab = await openURL({url, newTab: true});
|
||||
} else if (!same) {
|
||||
msg.sendTab(tab.id, {method: 'pushState', url: setUrlParams(tab.url)});
|
||||
}
|
||||
return activateTab(tab); // activateTab unminimizes the window
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
function onTabReady(tab) {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(function ping(numTries = 10, delay = 100) {
|
||||
msg.sendTab(tab.id, {method: 'ping'})
|
||||
.catch(() => false)
|
||||
.then(pong => pong
|
||||
? resolve(tab)
|
||||
: numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
|
||||
reject('timeout'));
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||
},
|
||||
};
|
||||
|
||||
if (chrome.commands) {
|
||||
chrome.commands.onCommand.addListener(id => browserCommands[id]());
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
||||
if (reason === 'install') {
|
||||
if (UA.mobile) prefs.set('manage.newUI', false);
|
||||
if (UA.windows) prefs.set('editor.keyMap', 'sublime');
|
||||
}
|
||||
// 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') {
|
||||
for (const dbName of ['drafts', prefs.STORAGE_KEY]) {
|
||||
try {
|
||||
indexedDB.open(dbName).onsuccess = async e => {
|
||||
const idb = /** @type IDBDatabase */ e.target.result;
|
||||
const ta = idb.objectStoreNames[0] === 'data' && idb.transaction(['data']);
|
||||
if (ta && ta.objectStore('data').autoIncrement) {
|
||||
ta.abort();
|
||||
idb.close();
|
||||
await new Promise(setTimeout);
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
}
|
||||
};
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
msg.on((msg, sender) => {
|
||||
if (msg.method === 'invokeAPI') {
|
||||
let res = msg.path.reduce((res, name) => res && res[name], API);
|
||||
if (!res) throw new Error(`Unknown API.${msg.path.join('.')}`);
|
||||
res = res.apply({msg, sender}, msg.args);
|
||||
return res === undefined ? null : res;
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
Promise.all([
|
||||
browser.extension.isAllowedFileSchemeAccess()
|
||||
.then(res => API.data.set('hasFileAccess', res)),
|
||||
bgReady.styles,
|
||||
/* These are loaded conditionally.
|
||||
Each item uses `require` individually so IDE can jump to the source and track usage. */
|
||||
FIREFOX &&
|
||||
require(['/background/style-via-api']),
|
||||
FIREFOX && ((browser.commands || {}).update) &&
|
||||
require(['/background/browser-cmd-hotkeys']),
|
||||
!FIREFOX &&
|
||||
require(['/background/content-scripts']),
|
||||
chrome.contextMenus &&
|
||||
require(['/background/context-menus']),
|
||||
]).then(() => {
|
||||
bgReady._resolveAll();
|
||||
msg.ready = true;
|
||||
msg.broadcast({method: 'backgroundReady'});
|
||||
});
|
|
@ -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'}],
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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 +0,0 @@
|
|||
/* global API */// msg.js
|
||||
/* global addAPI */// common.js
|
||||
/* global isEmptyObj */// toolbox.js
|
||||
/* global prefs */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Uses chrome.tabs.insertCSS
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const ACTIONS = {
|
||||
styleApply,
|
||||
styleDeleted,
|
||||
styleUpdated,
|
||||
styleAdded,
|
||||
styleReplaceAll,
|
||||
prefChanged,
|
||||
updateCount,
|
||||
};
|
||||
const NOP = new Error('NOP');
|
||||
const onError = () => {};
|
||||
/* <tabId>: Object
|
||||
<frameId>: Object
|
||||
url: String, non-enumerable
|
||||
<styleId>: Array of strings
|
||||
section code */
|
||||
const cache = new Map();
|
||||
let observingTabs = false;
|
||||
|
||||
addAPI(/** @namespace API */ {
|
||||
async styleViaAPI(request) {
|
||||
try {
|
||||
const fn = ACTIONS[request.method];
|
||||
return fn ? fn(request, this.sender) : NOP;
|
||||
} catch (e) {}
|
||||
maybeToggleObserver();
|
||||
},
|
||||
});
|
||||
|
||||
function updateCount(request, sender) {
|
||||
const {tab, frameId} = sender;
|
||||
if (frameId) {
|
||||
throw new Error('we do not count styles for frames');
|
||||
}
|
||||
const {frameStyles} = getCachedData(tab.id, frameId);
|
||||
API.updateIconBadge.call({sender}, Object.keys(frameStyles));
|
||||
}
|
||||
|
||||
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
||||
if (prefs.get('disableAll')) {
|
||||
return NOP;
|
||||
}
|
||||
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
||||
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
||||
return NOP;
|
||||
}
|
||||
return API.styles.getSectionsByUrl(url, id).then(sections => {
|
||||
delete sections.cfg;
|
||||
const tasks = [];
|
||||
for (const section of Object.values(sections)) {
|
||||
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}) {
|
||||
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
|
||||
const code = styleSections.join('\n');
|
||||
if (code && !duplicateCodeExists({frameStyles, id, code})) {
|
||||
delete frameStyles[id];
|
||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
|
||||
return removeCSS(tab.id, frameId, code)
|
||||
.then(() => updateCount(null, {tab, frameId}));
|
||||
} else {
|
||||
return NOP;
|
||||
}
|
||||
}
|
||||
|
||||
function styleUpdated({style}, sender) {
|
||||
if (!style.enabled) {
|
||||
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) {
|
||||
return style.enabled ? styleApply(style, sender) : NOP;
|
||||
}
|
||||
|
||||
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) {
|
||||
if ('disableAll' in prefs) {
|
||||
if (!prefs.disableAll) {
|
||||
return styleApply({}, sender);
|
||||
}
|
||||
const {tab, frameId} = sender;
|
||||
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];
|
||||
if (isEmptyObj(tabFrames)) {
|
||||
onTabRemoved(tabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onTabRemoved(tabId) {
|
||||
cache.delete(tabId);
|
||||
maybeToggleObserver();
|
||||
}
|
||||
|
||||
function onTabReplaced(addedTabId, removedTabId) {
|
||||
onTabRemoved(removedTabId);
|
||||
}
|
||||
|
||||
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
|
||||
if (isEmptyObj(frameStyles)) {
|
||||
delete tabFrames[frameId];
|
||||
if (isEmptyObj(tabFrames)) {
|
||||
cache.delete(tabId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getCachedData(tabId, frameId, styleId) {
|
||||
const tabFrames = cache.get(tabId) || {};
|
||||
const frameStyles = tabFrames[frameId] || {};
|
||||
const styleSections = styleId && frameStyles[styleId] || [];
|
||||
return {tabFrames, frameStyles, styleSections};
|
||||
}
|
||||
|
||||
function getFrameStylesJoined({
|
||||
tab,
|
||||
frameId,
|
||||
frameStyles = getCachedData(tab.id, frameId).frameStyles,
|
||||
}) {
|
||||
return Object.keys(frameStyles).map(id => frameStyles[id].join('\n'));
|
||||
}
|
||||
|
||||
function duplicateCodeExists({
|
||||
tab,
|
||||
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) {
|
||||
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
|
||||
.catch(onError);
|
||||
}
|
||||
})();
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
356
backup/fileSaveLoad.js
Normal file
356
backup/fileSaveLoad.js
Normal file
|
@ -0,0 +1,356 @@
|
|||
/* global messageBox, handleUpdate, applyOnMessage */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||
|
||||
|
||||
function importFromFile({fileTypeFilter, file} = {}) {
|
||||
return new Promise(resolve => {
|
||||
const fileInput = document.createElement('input');
|
||||
if (file) {
|
||||
readFile();
|
||||
return;
|
||||
}
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT;
|
||||
fileInput.acceptCharset = 'utf-8';
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.initialValue = fileInput.value;
|
||||
fileInput.onchange = readFile;
|
||||
fileInput.click();
|
||||
|
||||
function readFile() {
|
||||
if (file || fileInput.value !== fileInput.initialValue) {
|
||||
file = file || fileInput.files[0];
|
||||
if (file.size > 100e6) {
|
||||
console.warn("100MB backup? I don't believe you.");
|
||||
importFromString('').then(resolve);
|
||||
return;
|
||||
}
|
||||
document.body.style.cursor = 'wait';
|
||||
const fReader = new FileReader();
|
||||
fReader.onloadend = event => {
|
||||
fileInput.remove();
|
||||
importFromString(event.target.result).then(numStyles => {
|
||||
document.body.style.cursor = '';
|
||||
resolve(numStyles);
|
||||
});
|
||||
};
|
||||
fReader.readAsText(file, 'utf-8');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function importFromString(jsonString) {
|
||||
if (!BG) {
|
||||
onBackgroundReady().then(() => importFromString(jsonString));
|
||||
return;
|
||||
}
|
||||
// create objects in background context
|
||||
const json = BG.tryJSONparse(jsonString) || [];
|
||||
if (typeof json.slice != 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
|
||||
let oldDigests;
|
||||
chrome.storage.local.get(null, data => (oldDigests = data));
|
||||
|
||||
const stats = {
|
||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
||||
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
||||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
let lastRenderTime = performance.now();
|
||||
const renderQueue = [];
|
||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||
|
||||
return new Promise(proceed);
|
||||
|
||||
function proceed(resolve) {
|
||||
while (index < json.length) {
|
||||
const item = json[index++];
|
||||
const info = analyze(item);
|
||||
if (info) {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
.then(style => account({style, info, resolve}));
|
||||
}
|
||||
}
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
renderQueue.length = 0;
|
||||
done(resolve);
|
||||
}
|
||||
|
||||
function analyze(item) {
|
||||
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||
|| (item.sections && typeof item.sections.slice != 'function')) {
|
||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||
return;
|
||||
}
|
||||
item.name = item.name.trim();
|
||||
const byId = BG.cachedStyles.byId.get(item.id);
|
||||
const byName = oldStylesByName.get(item.name);
|
||||
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
|
||||
if (oldStyle == byName && byName) {
|
||||
item.id = byName.id;
|
||||
}
|
||||
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||
const metaEqual = oldStyleKeys &&
|
||||
oldStyleKeys.length == Object.keys(item).length &&
|
||||
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||
if (metaEqual && codeEqual) {
|
||||
stats.unchanged.names.push(oldStyle.name);
|
||||
stats.unchanged.ids.push(oldStyle.id);
|
||||
return;
|
||||
}
|
||||
return {oldStyle, metaEqual, codeEqual};
|
||||
}
|
||||
|
||||
function account({style, info, resolve}) {
|
||||
renderQueue.push(style);
|
||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||
renderQueue.length = 0;
|
||||
lastRenderTime = performance.now();
|
||||
}
|
||||
setTimeout(proceed, 0, resolve);
|
||||
const {oldStyle, metaEqual, codeEqual} = info;
|
||||
if (!oldStyle) {
|
||||
stats.added.names.push(style.name);
|
||||
stats.added.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!metaEqual && !codeEqual) {
|
||||
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaAndCode.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!codeEqual) {
|
||||
stats.codeOnly.names.push(style.name);
|
||||
stats.codeOnly.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaOnly.ids.push(style.id);
|
||||
}
|
||||
|
||||
function done(resolve) {
|
||||
const numChanged = stats.metaAndCode.names.length +
|
||||
stats.metaOnly.names.length +
|
||||
stats.codeOnly.names.length +
|
||||
stats.added.names.length;
|
||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||
const report = Object.keys(stats)
|
||||
.filter(kind => stats[kind].names.length)
|
||||
.map(kind => {
|
||||
const {ids, names, legend} = stats[kind];
|
||||
const listItemsWithId = (name, i) =>
|
||||
$element({dataset: {id: ids[i]}, textContent: name});
|
||||
const listItems = name =>
|
||||
$element({textContent: name});
|
||||
const block =
|
||||
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
||||
$element({tag: 'summary', appendChild:
|
||||
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
||||
}),
|
||||
$element({tag: 'small', appendChild:
|
||||
names.map(ids ? listItemsWithId : listItems)
|
||||
}),
|
||||
]});
|
||||
return block;
|
||||
});
|
||||
scrollTo(0, 0);
|
||||
messageBox({
|
||||
title: t('importReportTitle'),
|
||||
contents: report.length ? report : t('importReportUnchanged'),
|
||||
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||
onshow: bindClick,
|
||||
}).then(({button, enter, esc}) => {
|
||||
if (button == 1) {
|
||||
undo();
|
||||
}
|
||||
});
|
||||
resolve(numChanged);
|
||||
});
|
||||
}
|
||||
|
||||
function undo() {
|
||||
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||
const newIds = [
|
||||
...stats.metaAndCode.ids,
|
||||
...stats.metaOnly.ids,
|
||||
...stats.codeOnly.ids,
|
||||
...stats.added.ids,
|
||||
];
|
||||
let resolve;
|
||||
index = 0;
|
||||
return new Promise(resolve_ => {
|
||||
resolve = resolve_;
|
||||
undoNextId();
|
||||
}).then(BG.refreshAllTabs)
|
||||
.then(() => messageBox({
|
||||
title: t('importReportUndoneTitle'),
|
||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||
buttons: [t('confirmOK')],
|
||||
}));
|
||||
function undoNextId() {
|
||||
if (index == newIds.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const id = newIds[index++];
|
||||
deleteStyleSafe({id, notify: false}).then(id => {
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
oldStyle.styleDigest = oldDigests[BG.DIGEST_KEY_PREFIX + id];
|
||||
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||
.then(undoNextId);
|
||||
} else {
|
||||
undoNextId();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindClick(box) {
|
||||
const highlightElement = event => {
|
||||
const styleElement = $('#style-' + event.target.dataset.id);
|
||||
if (styleElement) {
|
||||
scrollElementIntoView(styleElement);
|
||||
animateElement(styleElement, {className: 'highlight'});
|
||||
}
|
||||
};
|
||||
for (const block of $$('details')) {
|
||||
if (block.dataset.id != 'invalid') {
|
||||
block.style.cursor = 'pointer';
|
||||
block.onclick = highlightElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function limitString(s, limit = 100) {
|
||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||
}
|
||||
|
||||
function reportNameChange(oldStyle, newStyle) {
|
||||
return newStyle.name != oldStyle.name
|
||||
? oldStyle.name + ' —> ' + newStyle.name
|
||||
: oldStyle.name;
|
||||
}
|
||||
|
||||
function refreshAllTabs() {
|
||||
return getActiveTab().then(activeTab => new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query({}, tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
if (tab.id == activeTab.id) {
|
||||
applyOnMessage(message);
|
||||
} else {
|
||||
chrome.tabs.sendMessage(tab.id, message);
|
||||
}
|
||||
BG.updateIcon(tab, styles);
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$('#file-all-styles').onclick = () => {
|
||||
Promise.all([
|
||||
BG.chromeLocal.get(null),
|
||||
getStylesSafe(),
|
||||
]).then(([data, styles]) => {
|
||||
styles = styles.map(style => {
|
||||
const styleDigest = data[BG.DIGEST_KEY_PREFIX + style.id];
|
||||
return styleDigest ? Object.assign({styleDigest}, style) : style;
|
||||
});
|
||||
const text = JSON.stringify(styles, null, '\t');
|
||||
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||
return url;
|
||||
// for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600
|
||||
}).then(fetch)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
Object.assign(document.createElement('a'), {
|
||||
download: generateFileName(),
|
||||
href: objectURL,
|
||||
type: 'application/json',
|
||||
}).dispatchEvent(new MouseEvent('click'));
|
||||
setTimeout(() => URL.revokeObjectURL(objectURL));
|
||||
});
|
||||
|
||||
function generateFileName() {
|
||||
const today = new Date();
|
||||
const dd = ('0' + today.getDate()).substr(-2);
|
||||
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||
const yyyy = today.getFullYear();
|
||||
return `stylus-${mm}-${dd}-${yyyy}${STYLUS_BACKUP_FILE_EXT}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('#unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
||||
this.classList.toggle('dropzone', hasFiles);
|
||||
if (hasFiles) {
|
||||
event.preventDefault();
|
||||
clearTimeout(this.fadeoutTimer);
|
||||
this.classList.remove('fadeout');
|
||||
}
|
||||
},
|
||||
ondragend(event) {
|
||||
animateElement(this, {className: 'fadeout'}).then(() => {
|
||||
this.style.animationDuration = '';
|
||||
this.classList.remove('dropzone');
|
||||
});
|
||||
},
|
||||
ondragleave(event) {
|
||||
// Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds
|
||||
if (!event.screenX && !event.screenX) {
|
||||
this.ondragend();
|
||||
}
|
||||
},
|
||||
ondrop(event) {
|
||||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
if ($('#onlyUpdates input').checked) {
|
||||
$('#onlyUpdates input').click();
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
},
|
||||
});
|
|
@ -87,7 +87,6 @@
|
|||
|
||||
var lineBreak = /\r\n|[\n\r\u2028\u2029]/;
|
||||
var allLineBreaks = new RegExp(lineBreak.source, 'g');
|
||||
var MOZ_DOC = "@-moz-document";
|
||||
|
||||
function css_beautify(source_text, options) {
|
||||
options = options || {};
|
||||
|
@ -101,27 +100,27 @@
|
|||
var newlinesFromLastWSEat = 0;
|
||||
var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4;
|
||||
var indentCharacter = options.indent_char || ' ';
|
||||
var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines;
|
||||
var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline;
|
||||
var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
|
||||
var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules;
|
||||
var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator;
|
||||
space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator);
|
||||
var eol = options.eol ? options.eol : 'auto';
|
||||
var {
|
||||
preserve_newlines = false,
|
||||
selector_separator_newline = true,
|
||||
end_with_newline = false,
|
||||
newline_between_rules = true,
|
||||
space_around_combinator = true,
|
||||
indent_conditional = true,
|
||||
indent_mozdoc = true,
|
||||
newline_between_properties = true,
|
||||
newline_before_open_brace = false,
|
||||
newline_after_open_brace = true,
|
||||
newline_before_close_brace = true,
|
||||
} = options;
|
||||
|
||||
/* STYLUS: hack start */
|
||||
const defaultOption = (opt, defaultValue) => opt === undefined ? defaultValue : opt;
|
||||
var newline_between_properties = defaultOption(options.newline_between_properties, true);
|
||||
var newline_before_open_brace = defaultOption(options.newline_before_open_brace, false);
|
||||
var newline_after_open_brace = defaultOption(options.newline_after_open_brace, true);
|
||||
var newline_before_close_brace = defaultOption(options.newline_before_close_brace, true);
|
||||
var translatePos = (options.translate_positions || [])[0];
|
||||
var translatePosIndex = 0;
|
||||
var translatePosLine = translatePos && translatePos.line;
|
||||
var translatePosCol = translatePos && translatePos.ch;
|
||||
var inputPosLine = 0, inputPosCol = 0;
|
||||
var outputPosLine = 0, outputPosCol = 0;
|
||||
/* STYLUS: hack end */
|
||||
|
||||
if (options.indent_with_tabs) {
|
||||
indentCharacter = '\t';
|
||||
|
@ -305,11 +304,8 @@
|
|||
newline_before_open_brace ? print.newLine() : print.singleSpace();
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
if (!enteringConditionalGroup || (variableOrRule === MOZ_DOC ? indent_mozdoc : indent_conditional)) {
|
||||
indent();
|
||||
}
|
||||
if (!eatWhitespace(true)) {
|
||||
newline_after_open_brace || enteringConditionalGroup ? print.newLine() : print.singleSpace();
|
||||
newline_after_open_brace ? print.newLine() : print.singleSpace();
|
||||
}
|
||||
};
|
||||
print["}"] = function(newline) {
|
||||
|
@ -318,7 +314,7 @@
|
|||
}
|
||||
output.push('}');
|
||||
outputPosCol++;
|
||||
if (!eatWhitespace(true) && peek(true) != '}') {
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
};
|
||||
|
@ -366,8 +362,7 @@
|
|||
}
|
||||
}
|
||||
outputPosCol = 0;
|
||||
let i = output.length;
|
||||
let token;
|
||||
let i = output.length, token;
|
||||
while (--i >= 0 && (token = output[i]) != '\n') {
|
||||
outputPosCol += token.length;
|
||||
}
|
||||
|
@ -390,7 +385,6 @@
|
|||
var insideRule = false;
|
||||
var insidePropertyValue = false;
|
||||
var enteringConditionalGroup = false;
|
||||
var insideConditionalGroup = false;
|
||||
var top_ch = '';
|
||||
var last_top_ch = '';
|
||||
|
||||
|
@ -404,8 +398,17 @@
|
|||
if (!ch) {
|
||||
break;
|
||||
} else if (ch === '/' && peek() === '*') { /* css comment */
|
||||
var header = indentLevel === 0;
|
||||
|
||||
if (isAfterNewline || header) {
|
||||
print.newLine();
|
||||
}
|
||||
|
||||
print.text(eatComment());
|
||||
if (peek() !== ';') print.newLine();
|
||||
print.newLine();
|
||||
if (header) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === '/' && peek() === '/') { // single line comment
|
||||
if (!isAfterNewline && last_top_ch !== '{') {
|
||||
print.trim();
|
||||
|
@ -434,16 +437,13 @@
|
|||
print.singleSpace();
|
||||
}
|
||||
|
||||
variableOrRule = '@' + variableOrRule.replace(/\s$/, '');
|
||||
variableOrRule = variableOrRule.replace(/\s$/, '');
|
||||
|
||||
// might be a nesting at-rule
|
||||
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
|
||||
nestedLevel += 1;
|
||||
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
|
||||
enteringConditionalGroup = true;
|
||||
if (variableOrRule === MOZ_DOC ? !indent_mozdoc : !indent_conditional) {
|
||||
nestedLevel--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -462,12 +462,11 @@
|
|||
print.newLine(true);
|
||||
}
|
||||
} else {
|
||||
indent();
|
||||
print["{"](ch);
|
||||
// when entering conditional groups, only rulesets are allowed
|
||||
if (enteringConditionalGroup) {
|
||||
enteringConditionalGroup = false;
|
||||
insidePropertyValue = false;
|
||||
insideConditionalGroup = true;
|
||||
insideRule = (indentLevel > nestedLevel);
|
||||
} else {
|
||||
// otherwise, declarations are also allowed
|
||||
|
@ -479,14 +478,10 @@
|
|||
print["}"](true);
|
||||
insideRule = false;
|
||||
insidePropertyValue = false;
|
||||
if (nestedLevel && (indent_conditional || !insideConditionalGroup)) {
|
||||
if (nestedLevel) {
|
||||
nestedLevel--;
|
||||
}
|
||||
insideConditionalGroup = false;
|
||||
if (newlinesFromLastWSEat < 2
|
||||
&& newline_between_rules
|
||||
//&& indentLevel === 0
|
||||
&& peek(true) != '}') {
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === ":") {
|
||||
|
@ -507,11 +502,10 @@
|
|||
// sass nested pseudo-class don't use a space
|
||||
|
||||
// preserve space before pseudoclasses/pseudoelements, as it means "in any child"
|
||||
if (lookBack(" ") && outputPosCol && !/\s$/.test(output[output.length - 1])) {
|
||||
if (lookBack(" ") && output[output.length - 1] !== " ") {
|
||||
output.push(" ");
|
||||
outputPosCol++;
|
||||
}
|
||||
|
||||
if (peek() === ":") {
|
||||
// pseudo-element
|
||||
next();
|
||||
|
@ -559,7 +553,7 @@
|
|||
} else if (ch === ',') {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
if (!eatWhitespace(true) && selector_separator_newline && !insidePropertyValue && parenLevel < 1) {
|
||||
if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) {
|
||||
print.newLine();
|
||||
} else {
|
||||
print.singleSpace();
|
||||
|
@ -630,12 +624,12 @@
|
|||
// also in CONDITIONAL_GROUP_RULE below
|
||||
"@media": true,
|
||||
"@supports": true,
|
||||
[MOZ_DOC]: true
|
||||
"@document": true
|
||||
};
|
||||
css_beautify.CONDITIONAL_GROUP_RULE = {
|
||||
"@media": true,
|
||||
"@supports": true,
|
||||
[MOZ_DOC]: true
|
||||
"@document": true
|
||||
};
|
||||
|
||||
/*global define */
|
571
beautify/beautify-css.js
Normal file
571
beautify/beautify-css.js
Normal file
|
@ -0,0 +1,571 @@
|
|||
/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
|
||||
/*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation files
|
||||
(the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
CSS Beautifier
|
||||
---------------
|
||||
|
||||
Written by Harutyun Amirjanyan, (amirjanyan@gmail.com)
|
||||
|
||||
Based on code initially developed by: Einar Lielmanis, <einar@jsbeautifier.org>
|
||||
http://jsbeautifier.org/
|
||||
|
||||
Usage:
|
||||
css_beautify(source_text);
|
||||
css_beautify(source_text, options);
|
||||
|
||||
The options are (default in brackets):
|
||||
indent_size (4) — indentation size,
|
||||
indent_char (space) — character to indent with,
|
||||
preserve_newlines (default false) - whether existing line breaks should be preserved,
|
||||
selector_separator_newline (true) - separate selectors with newline or
|
||||
not (e.g. "a,\nbr" or "a, br")
|
||||
end_with_newline (false) - end with a newline
|
||||
newline_between_rules (true) - add a new line after every css rule
|
||||
space_around_selector_separator (false) - ensure space around selector separators:
|
||||
'>', '+', '~' (e.g. "a>b" -> "a > b")
|
||||
e.g
|
||||
|
||||
css_beautify(css_source_text, {
|
||||
'indent_size': 1,
|
||||
'indent_char': '\t',
|
||||
'selector_separator': ' ',
|
||||
'end_with_newline': false,
|
||||
'newline_between_rules': true,
|
||||
'space_around_selector_separator': true
|
||||
});
|
||||
*/
|
||||
|
||||
// http://www.w3.org/TR/CSS21/syndata.html#tokenization
|
||||
// http://www.w3.org/TR/css3-syntax/
|
||||
|
||||
(function() {
|
||||
|
||||
function mergeOpts(allOptions, targetType) {
|
||||
var finalOpts = {};
|
||||
var name;
|
||||
|
||||
for (name in allOptions) {
|
||||
if (name !== targetType) {
|
||||
finalOpts[name] = allOptions[name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//merge in the per type settings for the targetType
|
||||
if (targetType in allOptions) {
|
||||
for (name in allOptions[targetType]) {
|
||||
finalOpts[name] = allOptions[targetType][name];
|
||||
}
|
||||
}
|
||||
return finalOpts;
|
||||
}
|
||||
|
||||
var lineBreak = /\r\n|[\n\r\u2028\u2029]/;
|
||||
var allLineBreaks = new RegExp(lineBreak.source, 'g');
|
||||
|
||||
function css_beautify(source_text, options) {
|
||||
options = options || {};
|
||||
|
||||
// Allow the setting of language/file-type specific options
|
||||
// with inheritance of overall settings
|
||||
options = mergeOpts(options, 'css');
|
||||
|
||||
source_text = source_text || '';
|
||||
|
||||
var newlinesFromLastWSEat = 0;
|
||||
var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4;
|
||||
var indentCharacter = options.indent_char || ' ';
|
||||
var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines;
|
||||
var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline;
|
||||
var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
|
||||
var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules;
|
||||
var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator;
|
||||
space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator);
|
||||
var eol = options.eol ? options.eol : 'auto';
|
||||
|
||||
if (options.indent_with_tabs) {
|
||||
indentCharacter = '\t';
|
||||
indentSize = 1;
|
||||
}
|
||||
|
||||
if (eol === 'auto') {
|
||||
eol = '\n';
|
||||
if (source_text && lineBreak.test(source_text || '')) {
|
||||
eol = source_text.match(lineBreak)[0];
|
||||
}
|
||||
}
|
||||
|
||||
eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n');
|
||||
|
||||
// HACK: newline parsing inconsistent. This brute force normalizes the input.
|
||||
source_text = source_text.replace(allLineBreaks, '\n');
|
||||
|
||||
// tokenizer
|
||||
var whiteRe = /^\s+$/;
|
||||
|
||||
var pos = -1,
|
||||
ch;
|
||||
var parenLevel = 0;
|
||||
|
||||
function next() {
|
||||
ch = source_text.charAt(++pos);
|
||||
return ch || '';
|
||||
}
|
||||
|
||||
function peek(skipWhitespace) {
|
||||
var result = '';
|
||||
var prev_pos = pos;
|
||||
if (skipWhitespace) {
|
||||
eatWhitespace();
|
||||
}
|
||||
result = source_text.charAt(pos + 1) || '';
|
||||
pos = prev_pos - 1;
|
||||
next();
|
||||
return result;
|
||||
}
|
||||
|
||||
function eatString(endChars) {
|
||||
var start = pos;
|
||||
while (next()) {
|
||||
if (ch === "\\") {
|
||||
next();
|
||||
} else if (endChars.indexOf(ch) !== -1) {
|
||||
break;
|
||||
} else if (ch === "\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return source_text.substring(start, pos + 1);
|
||||
}
|
||||
|
||||
function peekString(endChar) {
|
||||
var prev_pos = pos;
|
||||
var str = eatString(endChar);
|
||||
pos = prev_pos - 1;
|
||||
next();
|
||||
return str;
|
||||
}
|
||||
|
||||
function eatWhitespace(preserve_newlines_local) {
|
||||
var result = 0;
|
||||
while (whiteRe.test(peek())) {
|
||||
next();
|
||||
if (ch === '\n' && preserve_newlines_local && preserve_newlines) {
|
||||
print.newLine(true);
|
||||
result++;
|
||||
}
|
||||
}
|
||||
newlinesFromLastWSEat = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function skipWhitespace() {
|
||||
var result = '';
|
||||
if (ch && whiteRe.test(ch)) {
|
||||
result = ch;
|
||||
}
|
||||
while (whiteRe.test(next())) {
|
||||
result += ch;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function eatComment(singleLine) {
|
||||
var start = pos;
|
||||
singleLine = peek() === "/";
|
||||
next();
|
||||
while (next()) {
|
||||
if (!singleLine && ch === "*" && peek() === "/") {
|
||||
next();
|
||||
break;
|
||||
} else if (singleLine && ch === "\n") {
|
||||
return source_text.substring(start, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return source_text.substring(start, pos) + ch;
|
||||
}
|
||||
|
||||
|
||||
function lookBack(str) {
|
||||
return source_text.substring(pos - str.length, pos).toLowerCase() ===
|
||||
str;
|
||||
}
|
||||
|
||||
// Nested pseudo-class if we are insideRule
|
||||
// and the next special character found opens
|
||||
// a new block
|
||||
function foundNestedPseudoClass() {
|
||||
var openParen = 0;
|
||||
for (var i = pos + 1; i < source_text.length; i++) {
|
||||
var ch = source_text.charAt(i);
|
||||
if (ch === "{") {
|
||||
return true;
|
||||
} else if (ch === '(') {
|
||||
// pseudoclasses can contain ()
|
||||
openParen += 1;
|
||||
} else if (ch === ')') {
|
||||
if (openParen === 0) {
|
||||
return false;
|
||||
}
|
||||
openParen -= 1;
|
||||
} else if (ch === ";" || ch === "}") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// printer
|
||||
var basebaseIndentString = source_text.match(/^[\t ]*/)[0];
|
||||
var singleIndent = new Array(indentSize + 1).join(indentCharacter);
|
||||
var indentLevel = 0;
|
||||
var nestedLevel = 0;
|
||||
|
||||
function indent() {
|
||||
indentLevel++;
|
||||
basebaseIndentString += singleIndent;
|
||||
}
|
||||
|
||||
function outdent() {
|
||||
indentLevel--;
|
||||
basebaseIndentString = basebaseIndentString.slice(0, -indentSize);
|
||||
}
|
||||
|
||||
var print = {};
|
||||
print["{"] = function(ch) {
|
||||
print.singleSpace();
|
||||
output.push(ch);
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
};
|
||||
print["}"] = function(newline) {
|
||||
if (newline) {
|
||||
print.newLine();
|
||||
}
|
||||
output.push('}');
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
};
|
||||
|
||||
print._lastCharWhitespace = function() {
|
||||
return whiteRe.test(output[output.length - 1]);
|
||||
};
|
||||
|
||||
print.newLine = function(keepWhitespace) {
|
||||
if (output.length) {
|
||||
if (!keepWhitespace && output[output.length - 1] !== '\n') {
|
||||
print.trim();
|
||||
} else if (output[output.length - 1] === basebaseIndentString) {
|
||||
output.pop();
|
||||
}
|
||||
output.push('\n');
|
||||
|
||||
if (basebaseIndentString) {
|
||||
output.push(basebaseIndentString);
|
||||
}
|
||||
}
|
||||
};
|
||||
print.singleSpace = function() {
|
||||
if (output.length && !print._lastCharWhitespace()) {
|
||||
output.push(' ');
|
||||
}
|
||||
};
|
||||
|
||||
print.preserveSingleSpace = function() {
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
};
|
||||
|
||||
print.trim = function() {
|
||||
while (print._lastCharWhitespace()) {
|
||||
output.pop();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var output = [];
|
||||
/*_____________________--------------------_____________________*/
|
||||
|
||||
var insideRule = false;
|
||||
var insidePropertyValue = false;
|
||||
var enteringConditionalGroup = false;
|
||||
var top_ch = '';
|
||||
var last_top_ch = '';
|
||||
|
||||
while (true) {
|
||||
var whitespace = skipWhitespace();
|
||||
var isAfterSpace = whitespace !== '';
|
||||
var isAfterNewline = whitespace.indexOf('\n') !== -1;
|
||||
last_top_ch = top_ch;
|
||||
top_ch = ch;
|
||||
|
||||
if (!ch) {
|
||||
break;
|
||||
} else if (ch === '/' && peek() === '*') { /* css comment */
|
||||
var header = indentLevel === 0;
|
||||
|
||||
if (isAfterNewline || header) {
|
||||
print.newLine();
|
||||
}
|
||||
|
||||
output.push(eatComment());
|
||||
print.newLine();
|
||||
if (header) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === '/' && peek() === '/') { // single line comment
|
||||
if (!isAfterNewline && last_top_ch !== '{') {
|
||||
print.trim();
|
||||
}
|
||||
print.singleSpace();
|
||||
output.push(eatComment());
|
||||
print.newLine();
|
||||
} else if (ch === '@') {
|
||||
print.preserveSingleSpace();
|
||||
|
||||
// deal with less propery mixins @{...}
|
||||
if (peek() === '{') {
|
||||
output.push(eatString('}'));
|
||||
} else {
|
||||
output.push(ch);
|
||||
|
||||
// strip trailing space, if present, for hash property checks
|
||||
var variableOrRule = peekString(": ,;{}()[]/='\"");
|
||||
|
||||
if (variableOrRule.match(/[ :]$/)) {
|
||||
// we have a variable or pseudo-class, add it and insert one space before continuing
|
||||
next();
|
||||
variableOrRule = eatString(": ").replace(/\s$/, '');
|
||||
output.push(variableOrRule);
|
||||
print.singleSpace();
|
||||
}
|
||||
|
||||
variableOrRule = variableOrRule.replace(/\s$/, '');
|
||||
|
||||
// might be a nesting at-rule
|
||||
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
|
||||
nestedLevel += 1;
|
||||
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
|
||||
enteringConditionalGroup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ch === '#' && peek() === '{') {
|
||||
print.preserveSingleSpace();
|
||||
output.push(eatString('}'));
|
||||
} else if (ch === '{') {
|
||||
if (peek(true) === '}') {
|
||||
eatWhitespace();
|
||||
next();
|
||||
print.singleSpace();
|
||||
output.push("{");
|
||||
print['}'](false);
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else {
|
||||
indent();
|
||||
print["{"](ch);
|
||||
// when entering conditional groups, only rulesets are allowed
|
||||
if (enteringConditionalGroup) {
|
||||
enteringConditionalGroup = false;
|
||||
insideRule = (indentLevel > nestedLevel);
|
||||
} else {
|
||||
// otherwise, declarations are also allowed
|
||||
insideRule = (indentLevel >= nestedLevel);
|
||||
}
|
||||
}
|
||||
} else if (ch === '}') {
|
||||
outdent();
|
||||
print["}"](true);
|
||||
insideRule = false;
|
||||
insidePropertyValue = false;
|
||||
if (nestedLevel) {
|
||||
nestedLevel--;
|
||||
}
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === ":") {
|
||||
eatWhitespace();
|
||||
if ((insideRule || enteringConditionalGroup) &&
|
||||
!(lookBack("&") || foundNestedPseudoClass()) &&
|
||||
!lookBack("(")) {
|
||||
// 'property: value' delimiter
|
||||
// which could be in a conditional group query
|
||||
output.push(':');
|
||||
if (!insidePropertyValue) {
|
||||
insidePropertyValue = true;
|
||||
print.singleSpace();
|
||||
}
|
||||
} else {
|
||||
// sass/less parent reference don't use a space
|
||||
// sass nested pseudo-class don't use a space
|
||||
|
||||
// preserve space before pseudoclasses/pseudoelements, as it means "in any child"
|
||||
if (lookBack(" ") && output[output.length - 1] !== " ") {
|
||||
output.push(" ");
|
||||
}
|
||||
if (peek() === ":") {
|
||||
// pseudo-element
|
||||
next();
|
||||
output.push("::");
|
||||
} else {
|
||||
// pseudo-class
|
||||
output.push(':');
|
||||
}
|
||||
}
|
||||
} else if (ch === '"' || ch === '\'') {
|
||||
print.preserveSingleSpace();
|
||||
output.push(eatString(ch));
|
||||
} else if (ch === ';') {
|
||||
insidePropertyValue = false;
|
||||
output.push(ch);
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
} else if (ch === '(') { // may be a url
|
||||
if (lookBack("url")) {
|
||||
output.push(ch);
|
||||
eatWhitespace();
|
||||
if (next()) {
|
||||
if (ch !== ')' && ch !== '"' && ch !== '\'') {
|
||||
output.push(eatString(')'));
|
||||
} else {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parenLevel++;
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
eatWhitespace();
|
||||
}
|
||||
} else if (ch === ')') {
|
||||
output.push(ch);
|
||||
parenLevel--;
|
||||
} else if (ch === ',') {
|
||||
output.push(ch);
|
||||
if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) {
|
||||
print.newLine();
|
||||
} else {
|
||||
print.singleSpace();
|
||||
}
|
||||
} else if ((ch === '>' || ch === '+' || ch === '~') &&
|
||||
!insidePropertyValue && parenLevel < 1) {
|
||||
//handle combinator spacing
|
||||
if (space_around_combinator) {
|
||||
print.singleSpace();
|
||||
output.push(ch);
|
||||
print.singleSpace();
|
||||
} else {
|
||||
output.push(ch);
|
||||
eatWhitespace();
|
||||
// squash extra whitespace
|
||||
if (ch && whiteRe.test(ch)) {
|
||||
ch = '';
|
||||
}
|
||||
}
|
||||
} else if (ch === ']') {
|
||||
output.push(ch);
|
||||
} else if (ch === '[') {
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
} else if (ch === '=') { // no whitespace before or after
|
||||
eatWhitespace();
|
||||
output.push('=');
|
||||
if (whiteRe.test(ch)) {
|
||||
ch = '';
|
||||
}
|
||||
} else {
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var sweetCode = '';
|
||||
if (basebaseIndentString) {
|
||||
sweetCode += basebaseIndentString;
|
||||
}
|
||||
|
||||
sweetCode += output.join('').replace(/[\r\n\t ]+$/, '');
|
||||
|
||||
// establish end_with_newline
|
||||
if (end_with_newline) {
|
||||
sweetCode += '\n';
|
||||
}
|
||||
|
||||
if (eol !== '\n') {
|
||||
sweetCode = sweetCode.replace(/[\n]/g, eol);
|
||||
}
|
||||
|
||||
return sweetCode;
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
|
||||
css_beautify.NESTED_AT_RULE = {
|
||||
"@page": true,
|
||||
"@font-face": true,
|
||||
"@keyframes": true,
|
||||
// also in CONDITIONAL_GROUP_RULE below
|
||||
"@media": true,
|
||||
"@supports": true,
|
||||
"@document": true
|
||||
};
|
||||
css_beautify.CONDITIONAL_GROUP_RULE = {
|
||||
"@media": true,
|
||||
"@supports": true,
|
||||
"@document": true
|
||||
};
|
||||
|
||||
/*global define */
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
|
||||
define([], function() {
|
||||
return {
|
||||
css_beautify: css_beautify
|
||||
};
|
||||
});
|
||||
} else if (typeof exports !== "undefined") {
|
||||
// Add support for CommonJS. Just put this file somewhere on your require.paths
|
||||
// and you will be able to `var html_beautify = require("beautify").html_beautify`.
|
||||
exports.css_beautify = css_beautify;
|
||||
} else if (typeof window !== "undefined") {
|
||||
// If we're running a web page and don't have either of the above, add our one global
|
||||
window.css_beautify = css_beautify;
|
||||
} else if (typeof global !== "undefined") {
|
||||
// If we don't even have window, try global.
|
||||
global.css_beautify = css_beautify;
|
||||
}
|
||||
|
||||
}());
|
64
codemirror-overwrites/addon/lint/css-lint.js
Normal file
64
codemirror-overwrites/addon/lint/css-lint.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Depends on csslint.js from https://github.com/stubbornella/csslint
|
||||
|
||||
// declare global: CSSLint
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.registerHelper("lint", "css", function(text) {
|
||||
var found = [];
|
||||
if (!window.CSSLint) return found;
|
||||
|
||||
/* STYLISH: hack start (part 1) */
|
||||
var rules = CSSLint.getRules();
|
||||
var allowedRules = ["display-property-grouping", "duplicate-properties", "empty-rules", "errors", "known-properties"];
|
||||
CSSLint.clearRules();
|
||||
rules.forEach(function(rule) {
|
||||
if (allowedRules.indexOf(rule.id) >= 0) {
|
||||
CSSLint.addRule(rule);
|
||||
}
|
||||
});
|
||||
/* STYLISH: hack end */
|
||||
|
||||
var results = CSSLint.verify(text), messages = results.messages, message = null;
|
||||
for ( var i = 0; i < messages.length; i++) {
|
||||
message = messages[i];
|
||||
|
||||
/* STYLISH: hack start (part 2) */
|
||||
if (message.type === 'warning') {
|
||||
// @font-face {font-family: 'Ampersand'; unicode-range: U+26;}
|
||||
if (message.message.indexOf('unicode-range') !== -1) {
|
||||
continue;
|
||||
}
|
||||
else if ( // color: hsl(210, 100%, 2.2%); or color: hsla(210, 100%, 2.2%, 0.3);
|
||||
message.message.startsWith('Expected (<color>) but found \'hsl') &&
|
||||
/hsla?\(\s*(-?\d+)%?\s*,\s*(-?\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%(\s*,\s*(-?\d+|-?\d*.\d+))?\s*\)/.test(message.message)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
//
|
||||
}
|
||||
/* STYLISH: hack end */
|
||||
|
||||
var startLine = message.line -1, endLine = message.line -1, startCol = message.col -1, endCol = message.col;
|
||||
found.push({
|
||||
from: CodeMirror.Pos(startLine, startCol),
|
||||
to: CodeMirror.Pos(endLine, endCol),
|
||||
message: message.message,
|
||||
severity : message.type
|
||||
});
|
||||
}
|
||||
return found;
|
||||
});
|
||||
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Highlighting text that matches the selection
|
||||
//
|
||||
|
@ -19,6 +19,12 @@
|
|||
// highlighting the matches. If annotateScrollbar is enabled, the occurences
|
||||
// will be highlighted on the scrollbar via the matchesonscrollbar addon.
|
||||
|
||||
/* STYLUS: hack start (part 1) */
|
||||
/* eslint curly: 1, brace-style:1, strict: 0, quotes: 0, semi: 1, indent: 1 */
|
||||
/* eslint no-var: 0, block-scoped-var: 0, no-redeclare: 0, no-unused-expressions: 1 */
|
||||
/* global CodeMirror, require, define */
|
||||
/* STYLUS: hack end (part 1) */
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
|
||||
|
@ -36,8 +42,7 @@
|
|||
wordsOnly: false,
|
||||
annotateScrollbar: false,
|
||||
showToken: false,
|
||||
trim: true,
|
||||
onUpdate: () => {}
|
||||
trim: true
|
||||
}
|
||||
|
||||
function State(options) {
|
||||
|
@ -47,7 +52,6 @@
|
|||
this.overlay = this.timeout = null;
|
||||
this.matchesonscroll = null;
|
||||
this.active = false;
|
||||
this.query = null;
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
|
||||
|
@ -90,24 +94,14 @@
|
|||
|
||||
function addOverlay(cm, query, hasBoundary, style) {
|
||||
var state = cm.state.matchHighlighter;
|
||||
if (state.query === query) {
|
||||
return;
|
||||
}
|
||||
removeOverlay(cm);
|
||||
state.query = query;
|
||||
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
|
||||
/* STYLUS: hack start (part 2) */
|
||||
cm.addOverlay(state.overlay = makeOverlay(cm, query, hasBoundary, style));
|
||||
/* STYLUS: hack end (part 2) */
|
||||
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
|
||||
var searchFor = hasBoundary ?
|
||||
new RegExp(
|
||||
(/[a-z]/i.test(query[0]) ? "\\b" : "") +
|
||||
query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") +
|
||||
(/[a-z]/i.test(query[query.length - 1]) ? "\\b" : ""),
|
||||
"m"
|
||||
) : query;
|
||||
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
|
||||
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
|
||||
{className: "CodeMirror-selection-highlight-scrollbar"});
|
||||
}
|
||||
state.options.onUpdate(cm, state);
|
||||
}
|
||||
|
||||
function removeOverlay(cm) {
|
||||
|
@ -120,7 +114,6 @@
|
|||
state.matchesonscroll = null;
|
||||
}
|
||||
}
|
||||
state.query = null;
|
||||
}
|
||||
|
||||
function highlightMatches(cm) {
|
||||
|
@ -131,23 +124,26 @@
|
|||
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
|
||||
while (start && re.test(line.charAt(start - 1))) --start;
|
||||
while (end < line.length && re.test(line.charAt(end))) ++end;
|
||||
if (start < end) {
|
||||
addOverlay(cm, line.slice(start, end), re, state.options.style);
|
||||
} else {
|
||||
/* STYLUS: hack start */
|
||||
const token = line.slice(start, end);
|
||||
if (token !== state.lastToken) {
|
||||
state.lastToken = token;
|
||||
removeOverlay(cm);
|
||||
if (token) {
|
||||
addOverlay(cm, token, re, state.options.style);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
removeOverlay(cm);
|
||||
/* STYLUS: hack end */
|
||||
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
||||
if (from.line != to.line) return;
|
||||
if (state.options.wordsOnly && !isWord(cm, from, to)) return;
|
||||
var selection = cm.getRange(from, to)
|
||||
if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
|
||||
if (selection.length >= state.options.minChars) {
|
||||
if (selection.length >= state.options.minChars)
|
||||
addOverlay(cm, selection, false, state.options.style);
|
||||
} else {
|
||||
removeOverlay(cm);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -173,11 +169,28 @@
|
|||
(stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
|
||||
}
|
||||
|
||||
function makeOverlay(query, hasBoundary, style) {
|
||||
function makeOverlay(cm, query, hasBoundary, style) {
|
||||
/* STYLUS: hack start (part 3) */
|
||||
const approvedClassName = `cm-${style}-approved`;
|
||||
let timer;
|
||||
let occurrences = 0;
|
||||
return {token: function(stream) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
occurrences = 0;
|
||||
timer = null;
|
||||
});
|
||||
if (stream.match(query) &&
|
||||
(!hasBoundary || boundariesAround(stream, hasBoundary)))
|
||||
(!hasBoundary || boundariesAround(stream, hasBoundary))) {
|
||||
occurrences++;
|
||||
if (occurrences == 1) {
|
||||
cm.display.wrapper.classList.remove(approvedClassName);
|
||||
} else if (occurrences == 2) {
|
||||
cm.display.wrapper.classList.add(approvedClassName);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
/* STYLUS: hack end (part 3) */
|
||||
stream.next();
|
||||
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
|
||||
}};
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -13,7 +13,7 @@
|
|||
|
||||
var noOptions = {};
|
||||
var nonWS = /[^\s\u00a0]/;
|
||||
var Pos = CodeMirror.Pos, cmp = CodeMirror.cmpPos;
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function firstNonWS(str) {
|
||||
var found = str.search(nonWS);
|
||||
|
@ -78,7 +78,7 @@
|
|||
var baseString = null;
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var whitespace = line.search(nonWS) === -1 ? line : line.slice(0, firstNonWS(line));
|
||||
var whitespace = line.slice(0, firstNonWS(line));
|
||||
if (baseString == null || baseString.length > whitespace.length) {
|
||||
baseString = whitespace;
|
||||
}
|
||||
|
@ -126,9 +126,7 @@
|
|||
if (i != end || lastLineHasText)
|
||||
self.replaceRange(lead + pad, Pos(i, 0));
|
||||
} else {
|
||||
var atCursor = cmp(self.getCursor("to"), to) == 0, empty = !self.somethingSelected()
|
||||
self.replaceRange(endString, to);
|
||||
if (atCursor) self.setSelection(empty ? to : self.getCursor("from"), to)
|
||||
self.replaceRange(startString, from);
|
||||
}
|
||||
});
|
||||
|
@ -174,6 +172,10 @@
|
|||
if (open == -1) return false
|
||||
var endLine = end == start ? startLine : self.getLine(end)
|
||||
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
|
||||
if (close == -1 && start != end) {
|
||||
endLine = self.getLine(--end);
|
||||
close = endLine.indexOf(endString);
|
||||
}
|
||||
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
|
||||
if (close == -1 ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Open simple dialogs on top of an editor. Relies on dialog.css.
|
||||
|
||||
|
@ -25,7 +25,6 @@
|
|||
} else { // Assuming it's a detached DOM element.
|
||||
dialog.appendChild(template);
|
||||
}
|
||||
CodeMirror.addClass(wrap, 'dialog-opened');
|
||||
return dialog;
|
||||
}
|
||||
|
||||
|
@ -48,7 +47,6 @@
|
|||
} else {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
|
||||
|
@ -82,9 +80,7 @@
|
|||
if (e.keyCode == 13) callback(inp.value, e);
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(dialog, "focusout", function (evt) {
|
||||
if (evt.relatedTarget !== null) close();
|
||||
});
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
|
||||
} else if (button = dialog.getElementsByTagName("button")[0]) {
|
||||
CodeMirror.on(button, "click", function() {
|
||||
close();
|
||||
|
@ -106,7 +102,6 @@
|
|||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
}
|
||||
|
@ -146,7 +141,6 @@
|
|||
if (closed) return;
|
||||
closed = true;
|
||||
clearTimeout(doneTimer);
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -14,31 +14,17 @@
|
|||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
|
||||
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
|
||||
|
||||
function bracketRegex(config) {
|
||||
return config && config.bracketRegex || /[(){}[\]]/
|
||||
}
|
||||
|
||||
function findMatchingBracket(cm, where, config) {
|
||||
function findMatchingBracket(cm, where, strict, config) {
|
||||
var line = cm.getLineHandle(where.line), pos = where.ch - 1;
|
||||
var afterCursor = config && config.afterCursor
|
||||
if (afterCursor == null)
|
||||
afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
|
||||
var re = bracketRegex(config)
|
||||
|
||||
// A cursor is defined as between two characters, but in in vim command mode
|
||||
// (i.e. not insert mode), the cursor is visually represented as a
|
||||
// highlighted box on top of the 2nd character. Otherwise, we allow matches
|
||||
// from before or after the cursor.
|
||||
var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
|
||||
re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
|
||||
var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
|
||||
if (!match) return null;
|
||||
var dir = match.charAt(1) == ">" ? 1 : -1;
|
||||
if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
|
||||
if (strict && (dir > 0) != (pos == where.ch)) return null;
|
||||
var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
|
||||
|
||||
var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style, config);
|
||||
var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
|
||||
if (found == null) return null;
|
||||
return {from: Pos(where.line, pos), to: found && found.pos,
|
||||
match: found && found.ch == match.charAt(0), forward: dir > 0};
|
||||
|
@ -56,7 +42,7 @@
|
|||
var maxScanLines = (config && config.maxScanLines) || 1000;
|
||||
|
||||
var stack = [];
|
||||
var re = bracketRegex(config)
|
||||
var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
|
||||
var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
|
||||
: Math.max(cm.firstLine() - 1, where.line - maxScanLines);
|
||||
for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
|
||||
|
@ -67,10 +53,9 @@
|
|||
if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
|
||||
for (; pos != end; pos += dir) {
|
||||
var ch = line.charAt(pos);
|
||||
if (re.test(ch) && (style === undefined ||
|
||||
(cm.getTokenTypeAt(Pos(lineNo, pos + 1)) || "") == (style || ""))) {
|
||||
if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
|
||||
var match = matching[ch];
|
||||
if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
|
||||
if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
|
||||
else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
|
||||
else stack.pop();
|
||||
}
|
||||
|
@ -81,12 +66,11 @@
|
|||
|
||||
function matchBrackets(cm, autoclear, config) {
|
||||
// Disable brace matching in long lines, since it'll cause hugely slow updates
|
||||
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000,
|
||||
highlightNonMatching = config && config.highlightNonMatching;
|
||||
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
|
||||
var marks = [], ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
|
||||
if (match && (match.match || highlightNonMatching !== false) && cm.getLine(match.from.line).length <= maxHighlightLen) {
|
||||
var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
|
||||
if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
|
||||
var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
|
||||
marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
|
||||
if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
|
||||
|
@ -96,7 +80,7 @@
|
|||
|
||||
if (marks.length) {
|
||||
// Kludge to work around the IE bug from issue #1193, where text
|
||||
// input stops going to the textarea whenever this fires.
|
||||
// input stops going to the textare whever this fires.
|
||||
if (ie_lt8 && cm.state.focused) cm.focus();
|
||||
|
||||
var clear = function() {
|
||||
|
@ -109,50 +93,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
var currentlyHighlighted = null;
|
||||
function doMatchBrackets(cm) {
|
||||
cm.operation(function() {
|
||||
if (cm.state.matchBrackets.currentlyHighlighted) {
|
||||
cm.state.matchBrackets.currentlyHighlighted();
|
||||
cm.state.matchBrackets.currentlyHighlighted = null;
|
||||
}
|
||||
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
|
||||
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
|
||||
currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
|
||||
});
|
||||
}
|
||||
|
||||
function clearHighlighted(cm) {
|
||||
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
|
||||
cm.state.matchBrackets.currentlyHighlighted();
|
||||
cm.state.matchBrackets.currentlyHighlighted = null;
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.off("cursorActivity", doMatchBrackets);
|
||||
cm.off("focus", doMatchBrackets)
|
||||
cm.off("blur", clearHighlighted)
|
||||
clearHighlighted(cm);
|
||||
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
|
||||
}
|
||||
if (val) {
|
||||
cm.state.matchBrackets = typeof val == "object" ? val : {};
|
||||
cm.on("cursorActivity", doMatchBrackets);
|
||||
cm.on("focus", doMatchBrackets)
|
||||
cm.on("blur", clearHighlighted)
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
|
||||
CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
|
||||
// Backwards-compatibility kludge
|
||||
if (oldConfig || typeof config == "boolean") {
|
||||
if (!oldConfig) {
|
||||
config = config ? {strict: true} : null
|
||||
} else {
|
||||
oldConfig.strict = config
|
||||
config = oldConfig
|
||||
}
|
||||
}
|
||||
return findMatchingBracket(this, pos, config)
|
||||
CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
|
||||
return findMatchingBracket(this, pos, strict, config);
|
||||
});
|
||||
CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
|
||||
return scanForBracket(this, pos, dir, style, config);
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -11,14 +11,13 @@
|
|||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function bracketFolding(pairs) {
|
||||
return function(cm, start) {
|
||||
CodeMirror.registerHelper("fold", "brace", function(cm, start) {
|
||||
var line = start.line, lineText = cm.getLine(line);
|
||||
|
||||
function findOpening(pair) {
|
||||
var tokenType;
|
||||
|
||||
function findOpening(openCh) {
|
||||
for (var at = start.ch, pass = 0;;) {
|
||||
var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1);
|
||||
var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1);
|
||||
if (found == -1) {
|
||||
if (pass == 1) break;
|
||||
pass = 1;
|
||||
|
@ -27,51 +26,38 @@ function bracketFolding(pairs) {
|
|||
}
|
||||
if (pass == 1 && found < start.ch) break;
|
||||
tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
|
||||
if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair};
|
||||
if (!/^(comment|string)/.test(tokenType)) return found + 1;
|
||||
at = found - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(found) {
|
||||
var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh
|
||||
var startToken = "{", endToken = "}", startCh = findOpening("{");
|
||||
if (startCh == null) {
|
||||
startToken = "[", endToken = "]";
|
||||
startCh = findOpening("[");
|
||||
}
|
||||
|
||||
if (startCh == null) return;
|
||||
var count = 1, lastLine = cm.lastLine(), end, endCh;
|
||||
outer: for (var i = line; i <= lastLine; ++i) {
|
||||
var text = cm.getLine(i), pos = i == line ? startCh : 0;
|
||||
for (;;) {
|
||||
var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos);
|
||||
var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
|
||||
if (nextOpen < 0) nextOpen = text.length;
|
||||
if (nextClose < 0) nextClose = text.length;
|
||||
pos = Math.min(nextOpen, nextClose);
|
||||
if (pos == text.length) break;
|
||||
if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) {
|
||||
if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) {
|
||||
if (pos == nextOpen) ++count;
|
||||
else if (!--count) { end = i; endCh = pos; break outer; }
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (end == null || line == end) return null
|
||||
if (end == null || line == end && endCh == startCh) return;
|
||||
return {from: CodeMirror.Pos(line, startCh),
|
||||
to: CodeMirror.Pos(end, endCh)};
|
||||
}
|
||||
|
||||
var found = []
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var open = findOpening(pairs[i])
|
||||
if (open) found.push(open)
|
||||
}
|
||||
found.sort(function(a, b) { return a.ch - b.ch })
|
||||
for (var i = 0; i < found.length; i++) {
|
||||
var range = findRange(found[i])
|
||||
if (range) return range
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]]));
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "import", function(cm, start) {
|
||||
function hasImport(line) {
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -24,11 +24,9 @@
|
|||
function getRange(allowFolded) {
|
||||
var range = finder(cm, pos);
|
||||
if (!range || range.to.line - range.from.line < minSize) return null;
|
||||
if (force === "fold") return range;
|
||||
|
||||
var marks = cm.findMarksAt(range.from);
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
if (marks[i].__isFold && force !== "fold") {
|
||||
if (!allowFolded) return null;
|
||||
range.cleared = true;
|
||||
marks[i].clear();
|
||||
|
@ -44,7 +42,7 @@
|
|||
}
|
||||
if (!range || range.cleared || force === "unfold") return;
|
||||
|
||||
var myWidget = makeWidget(cm, options, range);
|
||||
var myWidget = makeWidget(cm, options);
|
||||
CodeMirror.on(myWidget, "mousedown", function(e) {
|
||||
myRange.clear();
|
||||
CodeMirror.e_preventDefault(e);
|
||||
|
@ -60,20 +58,13 @@
|
|||
CodeMirror.signal(cm, "fold", cm, range.from, range.to);
|
||||
}
|
||||
|
||||
function makeWidget(cm, options, range) {
|
||||
function makeWidget(cm, options) {
|
||||
var widget = getOption(cm, options, "widget");
|
||||
|
||||
if (typeof widget == "function") {
|
||||
widget = widget(range.from, range.to);
|
||||
}
|
||||
|
||||
if (typeof widget == "string") {
|
||||
var text = document.createTextNode(widget);
|
||||
widget = document.createElement("span");
|
||||
widget.appendChild(text);
|
||||
widget.className = "CodeMirror-foldmarker";
|
||||
} else if (widget) {
|
||||
widget = widget.cloneNode(true)
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
@ -101,18 +92,18 @@
|
|||
cm.foldCode(cm.getCursor(), null, "fold");
|
||||
};
|
||||
CodeMirror.commands.unfold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), { scanUp: false }, "unfold");
|
||||
cm.foldCode(cm.getCursor(), null, "unfold");
|
||||
};
|
||||
CodeMirror.commands.foldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "fold");
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), null, "fold");
|
||||
});
|
||||
};
|
||||
CodeMirror.commands.unfoldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "unfold");
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold");
|
||||
});
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -16,23 +16,21 @@
|
|||
cm.clearGutter(cm.state.foldGutter.options.gutter);
|
||||
cm.state.foldGutter = null;
|
||||
cm.off("gutterClick", onGutterClick);
|
||||
cm.off("changes", onChange);
|
||||
cm.off("change", onChange);
|
||||
cm.off("viewportChange", onViewportChange);
|
||||
cm.off("fold", onFold);
|
||||
cm.off("unfold", onFold);
|
||||
cm.off("swapDoc", onChange);
|
||||
cm.off("optionChange", optionChange);
|
||||
}
|
||||
if (val) {
|
||||
cm.state.foldGutter = new State(parseOptions(val));
|
||||
updateInViewport(cm);
|
||||
cm.on("gutterClick", onGutterClick);
|
||||
cm.on("changes", onChange);
|
||||
cm.on("change", onChange);
|
||||
cm.on("viewportChange", onViewportChange);
|
||||
cm.on("fold", onFold);
|
||||
cm.on("unfold", onFold);
|
||||
cm.on("swapDoc", onChange);
|
||||
cm.on("optionChange", optionChange);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -53,13 +51,8 @@
|
|||
|
||||
function isFolded(cm, line) {
|
||||
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
var fromPos = marks[i].find(-1);
|
||||
if (fromPos && fromPos.line === line)
|
||||
return marks[i];
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < marks.length; ++i)
|
||||
if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
|
||||
}
|
||||
|
||||
function marker(spec) {
|
||||
|
@ -73,36 +66,24 @@
|
|||
}
|
||||
|
||||
function updateFoldInfo(cm, from, to) {
|
||||
var opts = cm.state.foldGutter.options, cur = from - 1;
|
||||
var opts = cm.state.foldGutter.options, cur = from;
|
||||
var minSize = cm.foldOption(opts, "minFoldSize");
|
||||
var func = cm.foldOption(opts, "rangeFinder");
|
||||
// we can reuse the built-in indicator element if its className matches the new state
|
||||
var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded);
|
||||
var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen);
|
||||
cm.eachLine(from, to, function(line) {
|
||||
++cur;
|
||||
var mark = null;
|
||||
var old = line.gutterMarkers;
|
||||
if (old) old = old[opts.gutter];
|
||||
if (isFolded(cm, cur)) {
|
||||
if (clsFolded && old && clsFolded.test(old.className)) return;
|
||||
mark = marker(opts.indicatorFolded);
|
||||
} else {
|
||||
var pos = Pos(cur, 0);
|
||||
var range = func && func(cm, pos);
|
||||
if (range && range.to.line - range.from.line >= minSize) {
|
||||
if (clsOpen && old && clsOpen.test(old.className)) return;
|
||||
if (range && range.to.line - range.from.line >= minSize)
|
||||
mark = marker(opts.indicatorOpen);
|
||||
}
|
||||
}
|
||||
if (!mark && !old) return;
|
||||
cm.setGutterMarker(line, opts.gutter, mark);
|
||||
++cur;
|
||||
});
|
||||
}
|
||||
|
||||
// copied from CodeMirror/src/util/dom.js
|
||||
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
|
||||
|
||||
function updateInViewport(cm) {
|
||||
var vp = cm.getViewport(), state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
|
@ -119,11 +100,7 @@
|
|||
if (gutter != opts.gutter) return;
|
||||
var folded = isFolded(cm, line);
|
||||
if (folded) folded.clear();
|
||||
else cm.foldCode(Pos(line, 0), opts);
|
||||
}
|
||||
|
||||
function optionChange(cm, option) {
|
||||
if (option == "mode") onChange(cm)
|
||||
else cm.foldCode(Pos(line, 0), opts.rangeFinder);
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -11,15 +11,9 @@
|
|||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var pseudoClasses = {"active":1, "after":1, "before":1, "checked":1, "default":1,
|
||||
"disabled":1, "empty":1, "enabled":1, "first-child":1, "first-letter":1,
|
||||
"first-line":1, "first-of-type":1, "focus":1, "hover":1, "in-range":1,
|
||||
"indeterminate":1, "invalid":1, "lang":1, "last-child":1, "last-of-type":1,
|
||||
"link":1, "not":1, "nth-child":1, "nth-last-child":1, "nth-last-of-type":1,
|
||||
"nth-of-type":1, "only-of-type":1, "only-child":1, "optional":1, "out-of-range":1,
|
||||
"placeholder":1, "read-only":1, "read-write":1, "required":1, "root":1,
|
||||
"selection":1, "target":1, "valid":1, "visited":1
|
||||
};
|
||||
var pseudoClasses = {link: 1, visited: 1, active: 1, hover: 1, focus: 1,
|
||||
"first-letter": 1, "first-line": 1, "first-child": 1,
|
||||
before: 1, after: 1, lang: 1};
|
||||
|
||||
CodeMirror.registerHelper("hint", "css", function(cm) {
|
||||
var cur = cm.getCursor(), token = cm.getTokenAt(cur);
|
|
@ -1,7 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
// declare global: DOMRect
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -48,10 +46,6 @@
|
|||
completion.update(true);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("closeHint", function() {
|
||||
if (this.state.completionActive) this.state.completionActive.close()
|
||||
})
|
||||
|
||||
function Completion(cm, options) {
|
||||
this.cm = cm;
|
||||
this.options = options;
|
||||
|
@ -61,11 +55,9 @@
|
|||
this.startPos = this.cm.getCursor("start");
|
||||
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
|
||||
|
||||
if (this.options.updateOnCursorActivity) {
|
||||
var self = this;
|
||||
cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
|
||||
}
|
||||
}
|
||||
|
||||
var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
|
||||
return setTimeout(fn, 1000/60);
|
||||
|
@ -77,9 +69,7 @@
|
|||
if (!this.active()) return;
|
||||
this.cm.state.completionActive = null;
|
||||
this.tick = null;
|
||||
if (this.options.updateOnCursorActivity) {
|
||||
this.cm.off("cursorActivity", this.activityFunc);
|
||||
}
|
||||
|
||||
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
|
||||
if (this.widget) this.widget.close();
|
||||
|
@ -91,19 +81,12 @@
|
|||
},
|
||||
|
||||
pick: function(data, i) {
|
||||
var completion = data.list[i], self = this;
|
||||
this.cm.operation(function() {
|
||||
if (completion.hint)
|
||||
completion.hint(self.cm, data, completion);
|
||||
else
|
||||
self.cm.replaceRange(getText(completion), completion.from || data.from,
|
||||
var completion = data.list[i];
|
||||
if (completion.hint) completion.hint(this.cm, data, completion);
|
||||
else this.cm.replaceRange(getText(completion), completion.from || data.from,
|
||||
completion.to || data.to, "complete");
|
||||
CodeMirror.signal(data, "pick", completion);
|
||||
self.cm.scrollIntoView();
|
||||
});
|
||||
if (this.options.closeOnPick) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
cursorActivity: function() {
|
||||
|
@ -112,15 +95,10 @@
|
|||
this.debounce = 0;
|
||||
}
|
||||
|
||||
var identStart = this.startPos;
|
||||
if(this.data) {
|
||||
identStart = this.data.from;
|
||||
}
|
||||
|
||||
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
|
||||
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
|
||||
pos.ch < identStart.ch || this.cm.somethingSelected() ||
|
||||
(!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
|
||||
pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
|
||||
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
|
||||
this.close();
|
||||
} else {
|
||||
var self = this;
|
||||
|
@ -143,6 +121,7 @@
|
|||
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
|
||||
if (this.widget) this.widget.close();
|
||||
|
||||
if (data && this.data && isNewCompletion(this.data, data)) return;
|
||||
this.data = data;
|
||||
|
||||
if (data && data.list.length) {
|
||||
|
@ -156,6 +135,11 @@
|
|||
}
|
||||
};
|
||||
|
||||
function isNewCompletion(old, nw) {
|
||||
var moved = CodeMirror.cmpPos(nw.from, old.from)
|
||||
return moved > 0 && old.to.ch - old.from.ch != nw.to.ch - nw.from.ch
|
||||
}
|
||||
|
||||
function parseOptions(cm, pos, options) {
|
||||
var editor = cm.options.hintOptions;
|
||||
var out = {};
|
||||
|
@ -185,14 +169,6 @@
|
|||
Tab: handle.pick,
|
||||
Esc: handle.close
|
||||
};
|
||||
|
||||
var mac = /Mac/.test(navigator.platform);
|
||||
|
||||
if (mac) {
|
||||
baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);};
|
||||
baseMap["Ctrl-N"] = function() {handle.moveFocus(1);};
|
||||
}
|
||||
|
||||
var custom = completion.options.customKeys;
|
||||
var ourMap = custom ? {} : baseMap;
|
||||
function addBinding(key, val) {
|
||||
|
@ -224,92 +200,61 @@
|
|||
}
|
||||
|
||||
function Widget(completion, data) {
|
||||
this.id = "cm-complete-" + Math.floor(Math.random(1e6))
|
||||
this.completion = completion;
|
||||
this.data = data;
|
||||
this.picked = false;
|
||||
var widget = this, cm = completion.cm;
|
||||
var ownerDocument = cm.getInputField().ownerDocument;
|
||||
var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow;
|
||||
|
||||
var hints = this.hints = ownerDocument.createElement("ul");
|
||||
hints.setAttribute("role", "listbox")
|
||||
hints.setAttribute("aria-expanded", "true")
|
||||
hints.id = this.id
|
||||
var theme = completion.cm.options.theme;
|
||||
hints.className = "CodeMirror-hints " + theme;
|
||||
var hints = this.hints = document.createElement("ul");
|
||||
hints.className = "CodeMirror-hints";
|
||||
this.selectedHint = data.selectedHint || 0;
|
||||
|
||||
var completions = data.list;
|
||||
for (var i = 0; i < completions.length; ++i) {
|
||||
var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i];
|
||||
var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
|
||||
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
|
||||
if (cur.className != null) className = cur.className + " " + className;
|
||||
elt.className = className;
|
||||
if (i == this.selectedHint) elt.setAttribute("aria-selected", "true")
|
||||
elt.id = this.id + "-" + i
|
||||
elt.setAttribute("role", "option")
|
||||
if (cur.render) cur.render(elt, data, cur);
|
||||
else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur)));
|
||||
else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
|
||||
elt.hintId = i;
|
||||
}
|
||||
|
||||
var container = completion.options.container || ownerDocument.body;
|
||||
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
|
||||
var left = pos.left, top = pos.bottom, below = true;
|
||||
var offsetLeft = 0, offsetTop = 0;
|
||||
if (container !== ownerDocument.body) {
|
||||
// We offset the cursor position because left and top are relative to the offsetParent's top left corner.
|
||||
var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1;
|
||||
var offsetParent = isContainerPositioned ? container : container.offsetParent;
|
||||
var offsetParentPosition = offsetParent.getBoundingClientRect();
|
||||
var bodyPosition = ownerDocument.body.getBoundingClientRect();
|
||||
offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft);
|
||||
offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop);
|
||||
}
|
||||
hints.style.left = (left - offsetLeft) + "px";
|
||||
hints.style.top = (top - offsetTop) + "px";
|
||||
|
||||
hints.style.left = left + "px";
|
||||
hints.style.top = top + "px";
|
||||
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
|
||||
var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth);
|
||||
var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight);
|
||||
container.appendChild(hints);
|
||||
cm.getInputField().setAttribute("aria-autocomplete", "list")
|
||||
cm.getInputField().setAttribute("aria-owns", this.id)
|
||||
cm.getInputField().setAttribute("aria-activedescendant", this.id + "-" + this.selectedHint)
|
||||
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
|
||||
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
|
||||
(completion.options.container || document.body).appendChild(hints);
|
||||
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
|
||||
var scrolls = hints.scrollHeight > hints.clientHeight + 1
|
||||
var startScroll = cm.getScrollInfo();
|
||||
|
||||
var box = completion.options.moveOnOverlap ? hints.getBoundingClientRect() : new DOMRect();
|
||||
var scrolls = completion.options.paddingForScrollbar ? hints.scrollHeight > hints.clientHeight + 1 : false;
|
||||
|
||||
// Compute in the timeout to avoid reflow on init
|
||||
var startScroll;
|
||||
setTimeout(function() { startScroll = cm.getScrollInfo(); });
|
||||
|
||||
var overlapY = box.bottom - winH;
|
||||
if (overlapY > 0) {
|
||||
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
|
||||
if (curTop - height > 0) { // Fits above cursor
|
||||
hints.style.top = (top = pos.top - height - offsetTop) + "px";
|
||||
hints.style.top = (top = pos.top - height) + "px";
|
||||
below = false;
|
||||
} else if (height > winH) {
|
||||
hints.style.height = (winH - 5) + "px";
|
||||
hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px";
|
||||
hints.style.top = (top = pos.bottom - box.top) + "px";
|
||||
var cursor = cm.getCursor();
|
||||
if (data.from.ch != cursor.ch) {
|
||||
pos = cm.cursorCoords(cursor);
|
||||
hints.style.left = (left = pos.left - offsetLeft) + "px";
|
||||
hints.style.left = (left = pos.left) + "px";
|
||||
box = hints.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
var overlapX = box.right - winW;
|
||||
if (scrolls) overlapX += cm.display.nativeBarWidth;
|
||||
if (overlapX > 0) {
|
||||
if (box.right - box.left > winW) {
|
||||
hints.style.width = (winW - 5) + "px";
|
||||
overlapX -= (box.right - box.left) - winW;
|
||||
}
|
||||
hints.style.left = (left = Math.max(pos.left - overlapX - offsetLeft, 0)) + "px";
|
||||
hints.style.left = (left = pos.left - overlapX) + "px";
|
||||
}
|
||||
if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
|
||||
node.style.paddingRight = cm.display.nativeBarWidth + "px"
|
||||
|
@ -332,9 +277,8 @@
|
|||
|
||||
cm.on("scroll", this.onScroll = function() {
|
||||
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
|
||||
if (!startScroll) startScroll = cm.getScrollInfo();
|
||||
var newTop = top + startScroll.top - curScroll.top;
|
||||
var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop);
|
||||
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
|
||||
if (!below) point += hints.offsetHeight;
|
||||
if (point <= editor.top || point >= editor.bottom) return completion.close();
|
||||
hints.style.top = newTop + "px";
|
||||
|
@ -358,13 +302,7 @@
|
|||
setTimeout(function(){cm.focus();}, 20);
|
||||
});
|
||||
|
||||
// The first hint doesn't need to be scrolled to on init
|
||||
var selectedHintRange = this.getSelectedHintRange();
|
||||
if (selectedHintRange.from !== 0 || selectedHintRange.to !== 0) {
|
||||
this.scrollToActive();
|
||||
}
|
||||
|
||||
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
|
||||
CodeMirror.signal(data, "select", completions[0], hints.firstChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -372,11 +310,8 @@
|
|||
close: function() {
|
||||
if (this.completion.widget != this) return;
|
||||
this.completion.widget = null;
|
||||
if (this.hints.parentNode) this.hints.parentNode.removeChild(this.hints);
|
||||
this.hints.parentNode.removeChild(this.hints);
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
var input = this.completion.cm.getInputField()
|
||||
input.removeAttribute("aria-activedescendant")
|
||||
input.removeAttribute("aria-owns")
|
||||
|
||||
var cm = this.completion.cm;
|
||||
if (this.completion.options.closeOnUnfocus) {
|
||||
|
@ -404,39 +339,18 @@
|
|||
i = avoidWrap ? 0 : this.data.list.length - 1;
|
||||
if (this.selectedHint == i) return;
|
||||
var node = this.hints.childNodes[this.selectedHint];
|
||||
if (node) {
|
||||
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
|
||||
node.removeAttribute("aria-selected")
|
||||
}
|
||||
node = this.hints.childNodes[this.selectedHint = i];
|
||||
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
|
||||
node.setAttribute("aria-selected", "true")
|
||||
this.completion.cm.getInputField().setAttribute("aria-activedescendant", node.id)
|
||||
this.scrollToActive()
|
||||
if (node.offsetTop < this.hints.scrollTop)
|
||||
this.hints.scrollTop = node.offsetTop - 3;
|
||||
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
|
||||
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
|
||||
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
|
||||
},
|
||||
|
||||
scrollToActive: function() {
|
||||
var selectedHintRange = this.getSelectedHintRange();
|
||||
var node1 = this.hints.childNodes[selectedHintRange.from];
|
||||
var node2 = this.hints.childNodes[selectedHintRange.to];
|
||||
var firstNode = this.hints.firstChild;
|
||||
if (node1.offsetTop < this.hints.scrollTop)
|
||||
this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop;
|
||||
else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
|
||||
this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop;
|
||||
},
|
||||
|
||||
screenAmount: function() {
|
||||
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
|
||||
},
|
||||
|
||||
getSelectedHintRange: function() {
|
||||
var margin = this.completion.options.scrollMargin || 0;
|
||||
return {
|
||||
from: Math.max(0, this.selectedHint - margin),
|
||||
to: Math.min(this.data.list.length - 1, this.selectedHint + margin),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -489,13 +403,12 @@
|
|||
});
|
||||
|
||||
CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
|
||||
var cur = cm.getCursor(), token = cm.getTokenAt(cur)
|
||||
var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
|
||||
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
|
||||
term = token.string.substr(0, cur.ch - token.start)
|
||||
var cur = cm.getCursor(), token = cm.getTokenAt(cur);
|
||||
var to = CodeMirror.Pos(cur.line, token.end);
|
||||
if (token.string && /\w/.test(token.string[token.string.length - 1])) {
|
||||
var term = token.string, from = CodeMirror.Pos(cur.line, token.start);
|
||||
} else {
|
||||
term = ""
|
||||
from = cur
|
||||
var term = "", from = to;
|
||||
}
|
||||
var found = [];
|
||||
for (var i = 0; i < options.words.length; i++) {
|
||||
|
@ -514,15 +427,11 @@
|
|||
completeSingle: true,
|
||||
alignWithWord: true,
|
||||
closeCharacters: /[\s()\[\]{};:>,]/,
|
||||
closeOnPick: true,
|
||||
closeOnUnfocus: true,
|
||||
updateOnCursorActivity: true,
|
||||
completeOnSingleClick: true,
|
||||
container: null,
|
||||
customKeys: null,
|
||||
extraKeys: null,
|
||||
paddingForScrollbar: true,
|
||||
moveOnOverlap: true,
|
||||
extraKeys: null
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("hintOptions", null);
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Depends on csslint.js from https://github.com/stubbornella/csslint
|
||||
|
||||
|
@ -15,15 +15,10 @@
|
|||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.registerHelper("lint", "css", function(text, options) {
|
||||
CodeMirror.registerHelper("lint", "css", function(text) {
|
||||
var found = [];
|
||||
if (!window.CSSLint) {
|
||||
if (window.console) {
|
||||
window.console.error("Error: window.CSSLint not defined, CodeMirror CSS linting cannot run.");
|
||||
}
|
||||
return found;
|
||||
}
|
||||
var results = CSSLint.verify(text, options), messages = results.messages, message = null;
|
||||
if (!window.CSSLint) return found;
|
||||
var results = CSSLint.verify(text), messages = results.messages, message = null;
|
||||
for ( var i = 0; i < messages.length; i++) {
|
||||
message = messages[i];
|
||||
var startLine = message.line -1, endLine = message.line -1, startCol = message.col -1, endCol = message.col;
|
|
@ -25,20 +25,22 @@
|
|||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark {
|
||||
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image:
|
||||
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
|
||||
;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker {
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
|
@ -49,31 +51,23 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message {
|
||||
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||
padding-left: 18px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-error {
|
||||
background-color: rgba(183, 76, 81, 0.08);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning {
|
||||
background-color: rgba(255, 211, 0, 0.1);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -11,15 +11,11 @@
|
|||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
var GUTTER_ID = "CodeMirror-lint-markers";
|
||||
var LINT_LINE_ID = "CodeMirror-lint-line-";
|
||||
|
||||
function showTooltip(cm, e, content) {
|
||||
function showTooltip(e, content) {
|
||||
var tt = document.createElement("div");
|
||||
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
|
||||
tt.className = "CodeMirror-lint-tooltip";
|
||||
tt.appendChild(content.cloneNode(true));
|
||||
if (cm.state.lint.options.selfContain)
|
||||
cm.getWrapperElement().appendChild(tt);
|
||||
else
|
||||
document.body.appendChild(tt);
|
||||
|
||||
function position(e) {
|
||||
|
@ -42,8 +38,8 @@
|
|||
setTimeout(function() { rm(tt); }, 600);
|
||||
}
|
||||
|
||||
function showTooltipFor(cm, e, content, node) {
|
||||
var tooltip = showTooltip(cm, e, content);
|
||||
function showTooltipFor(e, content, node) {
|
||||
var tooltip = showTooltip(e, content);
|
||||
function hide() {
|
||||
CodeMirror.off(node, "mouseout", hide);
|
||||
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
|
||||
|
@ -59,64 +55,39 @@
|
|||
CodeMirror.on(node, "mouseout", hide);
|
||||
}
|
||||
|
||||
function LintState(cm, conf, hasGutter) {
|
||||
function LintState(cm, options, hasGutter) {
|
||||
this.marked = [];
|
||||
if (conf instanceof Function) conf = {getAnnotations: conf};
|
||||
if (!conf || conf === true) conf = {};
|
||||
this.options = {};
|
||||
this.linterOptions = conf.options || {};
|
||||
for (var prop in defaults) this.options[prop] = defaults[prop];
|
||||
for (var prop in conf) {
|
||||
if (defaults.hasOwnProperty(prop)) {
|
||||
if (conf[prop] != null) this.options[prop] = conf[prop];
|
||||
} else if (!conf.options) {
|
||||
this.linterOptions[prop] = conf[prop];
|
||||
}
|
||||
}
|
||||
this.options = options;
|
||||
this.timeout = null;
|
||||
this.hasGutter = hasGutter;
|
||||
this.onMouseOver = function(e) { onMouseOver(cm, e); };
|
||||
this.waitingFor = 0
|
||||
}
|
||||
|
||||
var defaults = {
|
||||
highlightLines: false,
|
||||
tooltips: true,
|
||||
delay: 500,
|
||||
lintOnChange: true,
|
||||
getAnnotations: null,
|
||||
async: false,
|
||||
selfContain: null,
|
||||
formatAnnotation: null,
|
||||
onUpdateLinting: null
|
||||
function parseOptions(_cm, options) {
|
||||
if (options instanceof Function) return {getAnnotations: options};
|
||||
if (!options || options === true) options = {};
|
||||
return options;
|
||||
}
|
||||
|
||||
function clearMarks(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
|
||||
if (state.options.highlightLines) clearErrorLines(cm);
|
||||
for (var i = 0; i < state.marked.length; ++i)
|
||||
state.marked[i].clear();
|
||||
state.marked.length = 0;
|
||||
}
|
||||
|
||||
function clearErrorLines(cm) {
|
||||
cm.eachLine(function(line) {
|
||||
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
|
||||
if (has) cm.removeLineClass(line, "wrap", has[0]);
|
||||
})
|
||||
}
|
||||
|
||||
function makeMarker(cm, labels, severity, multiple, tooltips) {
|
||||
function makeMarker(labels, severity, multiple, tooltips) {
|
||||
var marker = document.createElement("div"), inner = marker;
|
||||
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
|
||||
marker.className = "CodeMirror-lint-marker-" + severity;
|
||||
if (multiple) {
|
||||
inner = marker.appendChild(document.createElement("div"));
|
||||
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
|
||||
inner.className = "CodeMirror-lint-marker-multiple";
|
||||
}
|
||||
|
||||
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
|
||||
showTooltipFor(cm, e, labels, inner);
|
||||
showTooltipFor(e, labels, inner);
|
||||
});
|
||||
|
||||
return marker;
|
||||
|
@ -140,16 +111,12 @@
|
|||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
var tip = document.createElement("div");
|
||||
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
|
||||
if (typeof ann.messageHTML != 'undefined') {
|
||||
tip.innerHTML = ann.messageHTML;
|
||||
} else {
|
||||
tip.className = "CodeMirror-lint-message-" + severity;
|
||||
tip.appendChild(document.createTextNode(ann.message));
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
|
||||
function lintAsync(cm, getAnnotations) {
|
||||
function lintAsync(cm, getAnnotations, passOptions) {
|
||||
var state = cm.state.lint
|
||||
var id = ++state.waitingFor
|
||||
function abort() {
|
||||
|
@ -161,37 +128,29 @@
|
|||
cm.off("change", abort)
|
||||
if (state.waitingFor != id) return
|
||||
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
|
||||
cm.operation(function() {updateLinting(cm, annotations)})
|
||||
}, state.linterOptions, cm);
|
||||
updateLinting(cm, annotations)
|
||||
}, passOptions, cm);
|
||||
}
|
||||
|
||||
function startLinting(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
var options = state.options;
|
||||
/*
|
||||
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
|
||||
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
|
||||
*/
|
||||
var state = cm.state.lint, options = state.options;
|
||||
var passOptions = options.options || options; // Support deprecated passing of `options` property in options
|
||||
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
|
||||
if (!getAnnotations) return;
|
||||
if (options.async || getAnnotations.async) {
|
||||
lintAsync(cm, getAnnotations)
|
||||
lintAsync(cm, getAnnotations, passOptions)
|
||||
} else {
|
||||
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
|
||||
if (!annotations) return;
|
||||
var annotations = getAnnotations(cm.getValue(), passOptions, cm);
|
||||
if (annotations.then) annotations.then(function(issues) {
|
||||
cm.operation(function() {updateLinting(cm, issues)})
|
||||
updateLinting(cm, issues);
|
||||
});
|
||||
else cm.operation(function() {updateLinting(cm, annotations)})
|
||||
else updateLinting(cm, annotations);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLinting(cm, annotationsNotSorted) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
var options = state.options;
|
||||
clearMarks(cm);
|
||||
var state = cm.state.lint, options = state.options;
|
||||
|
||||
var annotations = groupByLine(annotationsNotSorted);
|
||||
|
||||
|
@ -199,10 +158,6 @@
|
|||
var anns = annotations[line];
|
||||
if (!anns) continue;
|
||||
|
||||
// filter out duplicate messages
|
||||
var message = [];
|
||||
anns = anns.filter(function(item) { return message.indexOf(item.message) > -1 ? false : message.push(item.message) });
|
||||
|
||||
var maxSeverity = null;
|
||||
var tipLabel = state.hasGutter && document.createDocumentFragment();
|
||||
|
||||
|
@ -216,17 +171,14 @@
|
|||
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
|
||||
|
||||
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
|
||||
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
|
||||
className: "CodeMirror-lint-mark-" + severity,
|
||||
__annotation: ann
|
||||
}));
|
||||
}
|
||||
// use original annotations[line] to show multiple messages
|
||||
if (state.hasGutter)
|
||||
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, annotations[line].length > 1,
|
||||
options.tooltips));
|
||||
|
||||
if (options.highlightLines)
|
||||
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
|
||||
if (state.hasGutter)
|
||||
cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
|
||||
state.options.tooltips));
|
||||
}
|
||||
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
|
||||
}
|
||||
|
@ -235,17 +187,17 @@
|
|||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
clearTimeout(state.timeout);
|
||||
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
|
||||
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
|
||||
}
|
||||
|
||||
function popupTooltips(cm, annotations, e) {
|
||||
function popupTooltips(annotations, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
var tooltip = document.createDocumentFragment();
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var ann = annotations[i];
|
||||
tooltip.appendChild(annotationTooltip(ann));
|
||||
}
|
||||
showTooltipFor(cm, e, tooltip, target);
|
||||
showTooltipFor(e, tooltip, target);
|
||||
}
|
||||
|
||||
function onMouseOver(cm, e) {
|
||||
|
@ -259,7 +211,7 @@
|
|||
var ann = spans[i].__annotation;
|
||||
if (ann) annotations.push(ann);
|
||||
}
|
||||
if (annotations.length) popupTooltips(cm, annotations, e);
|
||||
if (annotations.length) popupTooltips(annotations, e);
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
||||
|
@ -275,8 +227,8 @@
|
|||
if (val) {
|
||||
var gutters = cm.getOption("gutters"), hasLintGutter = false;
|
||||
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
|
||||
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
|
||||
if (state.options.lintOnChange)
|
||||
var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
|
||||
if (state.options.lintOnChange !== false)
|
||||
cm.on("change", onChange);
|
||||
if (state.options.tooltips != false && state.options.tooltips != "gutter")
|
||||
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
|
||||
|
@ -286,6 +238,6 @@
|
|||
});
|
||||
|
||||
CodeMirror.defineExtension("performLint", function() {
|
||||
startLinting(this);
|
||||
if (this.state.lint) startLinting(this);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -43,7 +43,7 @@
|
|||
cm.on("markerAdded", this.resizeHandler);
|
||||
cm.on("markerCleared", this.resizeHandler);
|
||||
if (options.listenForChanges !== false)
|
||||
cm.on("changes", this.changeHandler = function() {
|
||||
cm.on("change", this.changeHandler = function() {
|
||||
scheduleRedraw(250);
|
||||
});
|
||||
}
|
||||
|
@ -72,16 +72,10 @@
|
|||
var wrapping = cm.getOption("lineWrapping");
|
||||
var singleLineH = wrapping && cm.defaultTextHeight() * 1.5;
|
||||
var curLine = null, curLineObj = null;
|
||||
|
||||
function getY(pos, top) {
|
||||
if (curLine != pos.line) {
|
||||
curLine = pos.line
|
||||
curLineObj = cm.getLineHandle(pos.line)
|
||||
var visual = cm.getLineHandleVisualStart(curLineObj)
|
||||
if (visual != curLineObj) {
|
||||
curLine = cm.getLineNumber(visual)
|
||||
curLineObj = visual
|
||||
}
|
||||
curLine = pos.line;
|
||||
curLineObj = cm.getLineHandle(curLine);
|
||||
}
|
||||
if ((curLineObj.widgets && curLineObj.widgets.length) ||
|
||||
(wrapping && curLineObj.height > singleLineH))
|
||||
|
@ -122,7 +116,7 @@
|
|||
this.cm.off("refresh", this.resizeHandler);
|
||||
this.cm.off("markerAdded", this.resizeHandler);
|
||||
this.cm.off("markerCleared", this.resizeHandler);
|
||||
if (this.changeHandler) this.cm.off("changes", this.changeHandler);
|
||||
if (this.changeHandler) this.cm.off("change", this.changeHandler);
|
||||
this.div.parentNode.removeChild(this.div);
|
||||
};
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
|
@ -46,7 +46,7 @@
|
|||
if (match.from.line >= this.gap.to) break;
|
||||
if (match.to.line >= this.gap.from) this.matches.splice(i--, 1);
|
||||
}
|
||||
var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), {caseFold: this.caseFold, multiline: this.options.multiline});
|
||||
var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold);
|
||||
var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES;
|
||||
while (cursor.findNext()) {
|
||||
var match = {from: cursor.from(), to: cursor.to()};
|
252
codemirror/addon/search/search.js
vendored
Normal file
252
codemirror/addon/search/search.js
vendored
Normal file
|
@ -0,0 +1,252 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Define search commands. Depends on dialog.js or another
|
||||
// implementation of the openDialog method.
|
||||
|
||||
// Replace works a little oddly -- it will do the replace on the next
|
||||
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
|
||||
// replace by making sure the match is no longer selected when hitting
|
||||
// Ctrl-G.
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function searchOverlay(query, caseInsensitive) {
|
||||
if (typeof query == "string")
|
||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
|
||||
else if (!query.global)
|
||||
query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
|
||||
|
||||
return {token: function(stream) {
|
||||
query.lastIndex = stream.pos;
|
||||
var match = query.exec(stream.string);
|
||||
if (match && match.index == stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return "searching";
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
function SearchState() {
|
||||
this.posFrom = this.posTo = this.lastQuery = this.query = null;
|
||||
this.overlay = null;
|
||||
}
|
||||
|
||||
function getSearchState(cm) {
|
||||
return cm.state.search || (cm.state.search = new SearchState());
|
||||
}
|
||||
|
||||
function queryCaseInsensitive(query) {
|
||||
return typeof query == "string" && query == query.toLowerCase();
|
||||
}
|
||||
|
||||
function getSearchCursor(cm, query, pos) {
|
||||
// Heuristic: if the query string is all lowercase, do a case insensitive search.
|
||||
return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
|
||||
}
|
||||
|
||||
function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
|
||||
cm.openDialog(text, onEnter, {
|
||||
value: deflt,
|
||||
selectValueOnOpen: true,
|
||||
closeOnEnter: false,
|
||||
onClose: function() { clearSearch(cm); },
|
||||
onKeyDown: onKeyDown
|
||||
});
|
||||
}
|
||||
|
||||
function dialog(cm, text, shortText, deflt, f) {
|
||||
if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
|
||||
else f(prompt(shortText, deflt));
|
||||
}
|
||||
|
||||
function confirmDialog(cm, text, shortText, fs) {
|
||||
if (cm.openConfirm) cm.openConfirm(text, fs);
|
||||
else if (confirm(shortText)) fs[0]();
|
||||
}
|
||||
|
||||
function parseString(string) {
|
||||
return string.replace(/\\(.)/g, function(_, ch) {
|
||||
if (ch == "n") return "\n"
|
||||
if (ch == "r") return "\r"
|
||||
return ch
|
||||
})
|
||||
}
|
||||
|
||||
function parseQuery(query) {
|
||||
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
|
||||
if (isRE) {
|
||||
try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
|
||||
catch(e) {} // Not a regular expression after all, do a string search
|
||||
} else {
|
||||
query = parseString(query)
|
||||
}
|
||||
if (typeof query == "string" ? query == "" : query.test(""))
|
||||
query = /x^/;
|
||||
return query;
|
||||
}
|
||||
|
||||
var queryDialog =
|
||||
'Search: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
|
||||
|
||||
function startSearch(cm, state, query) {
|
||||
state.queryText = query;
|
||||
state.query = parseQuery(query);
|
||||
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
|
||||
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
|
||||
cm.addOverlay(state.overlay);
|
||||
if (cm.showMatchesOnScrollbar) {
|
||||
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
||||
state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch(cm, rev, persistent, immediate) {
|
||||
var state = getSearchState(cm);
|
||||
if (state.query) return findNext(cm, rev);
|
||||
var q = cm.getSelection() || state.lastQuery;
|
||||
if (persistent && cm.openDialog) {
|
||||
var hiding = null
|
||||
var searchNext = function(query, event) {
|
||||
CodeMirror.e_stop(event);
|
||||
if (!query) return;
|
||||
if (query != state.queryText) {
|
||||
startSearch(cm, state, query);
|
||||
state.posFrom = state.posTo = cm.getCursor();
|
||||
}
|
||||
if (hiding) hiding.style.opacity = 1
|
||||
findNext(cm, event.shiftKey, function(_, to) {
|
||||
var dialog
|
||||
if (to.line < 3 && document.querySelector &&
|
||||
(dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
|
||||
dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
|
||||
(hiding = dialog).style.opacity = .4
|
||||
})
|
||||
};
|
||||
persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
|
||||
var keyName = CodeMirror.keyName(event)
|
||||
var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
|
||||
if (!cmd) cmd = cm.getOption('extraKeys')[keyName]
|
||||
if (cmd == "findNext" || cmd == "findPrev" ||
|
||||
cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
|
||||
CodeMirror.e_stop(event);
|
||||
startSearch(cm, getSearchState(cm), query);
|
||||
cm.execCommand(cmd);
|
||||
} else if (cmd == "find" || cmd == "findPersistent") {
|
||||
CodeMirror.e_stop(event);
|
||||
searchNext(query, event);
|
||||
}
|
||||
});
|
||||
if (immediate && q) {
|
||||
startSearch(cm, state, q);
|
||||
findNext(cm, rev);
|
||||
}
|
||||
} else {
|
||||
dialog(cm, queryDialog, "Search for:", q, function(query) {
|
||||
if (query && !state.query) cm.operation(function() {
|
||||
startSearch(cm, state, query);
|
||||
state.posFrom = state.posTo = cm.getCursor();
|
||||
findNext(cm, rev);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findNext(cm, rev, callback) {cm.operation(function() {
|
||||
var state = getSearchState(cm);
|
||||
var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
|
||||
if (!cursor.find(rev)) {
|
||||
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
|
||||
if (!cursor.find(rev)) return;
|
||||
}
|
||||
cm.setSelection(cursor.from(), cursor.to());
|
||||
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
|
||||
state.posFrom = cursor.from(); state.posTo = cursor.to();
|
||||
if (callback) callback(cursor.from(), cursor.to())
|
||||
});}
|
||||
|
||||
function clearSearch(cm) {cm.operation(function() {
|
||||
var state = getSearchState(cm);
|
||||
state.lastQuery = state.query;
|
||||
if (!state.query) return;
|
||||
state.query = state.queryText = null;
|
||||
cm.removeOverlay(state.overlay);
|
||||
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
||||
});}
|
||||
|
||||
var replaceQueryDialog =
|
||||
' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
|
||||
var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
|
||||
var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
|
||||
|
||||
function replaceAll(cm, query, text) {
|
||||
cm.operation(function() {
|
||||
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
|
||||
if (typeof query != "string") {
|
||||
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
|
||||
cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
||||
} else cursor.replace(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function replace(cm, all) {
|
||||
if (cm.getOption("readOnly")) return;
|
||||
var query = cm.getSelection() || getSearchState(cm).lastQuery;
|
||||
var dialogText = all ? "Replace all:" : "Replace:"
|
||||
dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
|
||||
if (!query) return;
|
||||
query = parseQuery(query);
|
||||
dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
|
||||
text = parseString(text)
|
||||
if (all) {
|
||||
replaceAll(cm, query, text)
|
||||
} else {
|
||||
clearSearch(cm);
|
||||
var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
|
||||
var advance = function() {
|
||||
var start = cursor.from(), match;
|
||||
if (!(match = cursor.findNext())) {
|
||||
cursor = getSearchCursor(cm, query);
|
||||
if (!(match = cursor.findNext()) ||
|
||||
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
|
||||
}
|
||||
cm.setSelection(cursor.from(), cursor.to());
|
||||
cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
|
||||
confirmDialog(cm, doReplaceConfirm, "Replace?",
|
||||
[function() {doReplace(match);}, advance,
|
||||
function() {replaceAll(cm, query, text)}]);
|
||||
};
|
||||
var doReplace = function(match) {
|
||||
cursor.replace(typeof query == "string" ? text :
|
||||
text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
||||
advance();
|
||||
};
|
||||
advance();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
|
||||
CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
|
||||
CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
|
||||
CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
|
||||
CodeMirror.commands.findNext = doSearch;
|
||||
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
|
||||
CodeMirror.commands.clearSearch = clearSearch;
|
||||
CodeMirror.commands.replace = replace;
|
||||
CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
|
||||
});
|
189
codemirror/addon/search/searchcursor.js
vendored
Normal file
189
codemirror/addon/search/searchcursor.js
vendored
Normal file
|
@ -0,0 +1,189 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function SearchCursor(doc, query, pos, caseFold) {
|
||||
this.atOccurrence = false; this.doc = doc;
|
||||
if (caseFold == null && typeof query == "string") caseFold = false;
|
||||
|
||||
pos = pos ? doc.clipPos(pos) : Pos(0, 0);
|
||||
this.pos = {from: pos, to: pos};
|
||||
|
||||
// The matches method is filled in based on the type of query.
|
||||
// It takes a position and a direction, and returns an object
|
||||
// describing the next occurrence of the query, or null if no
|
||||
// more matches were found.
|
||||
if (typeof query != "string") { // Regexp match
|
||||
if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
|
||||
this.matches = function(reverse, pos) {
|
||||
if (reverse) {
|
||||
query.lastIndex = 0;
|
||||
var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
|
||||
for (;;) {
|
||||
query.lastIndex = cutOff;
|
||||
var newMatch = query.exec(line);
|
||||
if (!newMatch) break;
|
||||
match = newMatch;
|
||||
start = match.index;
|
||||
cutOff = match.index + (match[0].length || 1);
|
||||
if (cutOff == line.length) break;
|
||||
}
|
||||
var matchLen = (match && match[0].length) || 0;
|
||||
if (!matchLen) {
|
||||
if (start == 0 && line.length == 0) {match = undefined;}
|
||||
else if (start != doc.getLine(pos.line).length) {
|
||||
matchLen++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query.lastIndex = pos.ch;
|
||||
var line = doc.getLine(pos.line), match = query.exec(line);
|
||||
var matchLen = (match && match[0].length) || 0;
|
||||
var start = match && match.index;
|
||||
if (start + matchLen != line.length && !matchLen) matchLen = 1;
|
||||
}
|
||||
if (match && matchLen)
|
||||
return {from: Pos(pos.line, start),
|
||||
to: Pos(pos.line, start + matchLen),
|
||||
match: match};
|
||||
};
|
||||
} else { // String query
|
||||
var origQuery = query;
|
||||
if (caseFold) query = query.toLowerCase();
|
||||
var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
|
||||
var target = query.split("\n");
|
||||
// Different methods for single-line and multi-line queries
|
||||
if (target.length == 1) {
|
||||
if (!query.length) {
|
||||
// Empty string would match anything and never progress, so
|
||||
// we define it to match nothing instead.
|
||||
this.matches = function() {};
|
||||
} else {
|
||||
this.matches = function(reverse, pos) {
|
||||
if (reverse) {
|
||||
var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
|
||||
var match = line.lastIndexOf(query);
|
||||
if (match > -1) {
|
||||
match = adjustPos(orig, line, match);
|
||||
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
|
||||
}
|
||||
} else {
|
||||
var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
|
||||
var match = line.indexOf(query);
|
||||
if (match > -1) {
|
||||
match = adjustPos(orig, line, match) + pos.ch;
|
||||
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
var origTarget = origQuery.split("\n");
|
||||
this.matches = function(reverse, pos) {
|
||||
var last = target.length - 1;
|
||||
if (reverse) {
|
||||
if (pos.line - (target.length - 1) < doc.firstLine()) return;
|
||||
if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
|
||||
var to = Pos(pos.line, origTarget[last].length);
|
||||
for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
|
||||
if (target[i] != fold(doc.getLine(ln))) return;
|
||||
var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
|
||||
if (fold(line.slice(cut)) != target[0]) return;
|
||||
return {from: Pos(ln, cut), to: to};
|
||||
} else {
|
||||
if (pos.line + (target.length - 1) > doc.lastLine()) return;
|
||||
var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
|
||||
if (fold(line.slice(cut)) != target[0]) return;
|
||||
var from = Pos(pos.line, cut);
|
||||
for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
|
||||
if (target[i] != fold(doc.getLine(ln))) return;
|
||||
if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
|
||||
return {from: from, to: Pos(ln, origTarget[last].length)};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchCursor.prototype = {
|
||||
findNext: function() {return this.find(false);},
|
||||
findPrevious: function() {return this.find(true);},
|
||||
|
||||
find: function(reverse) {
|
||||
var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
|
||||
function savePosAndFail(line) {
|
||||
var pos = Pos(line, 0);
|
||||
self.pos = {from: pos, to: pos};
|
||||
self.atOccurrence = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
if (this.pos = this.matches(reverse, pos)) {
|
||||
this.atOccurrence = true;
|
||||
return this.pos.match || true;
|
||||
}
|
||||
if (reverse) {
|
||||
if (!pos.line) return savePosAndFail(0);
|
||||
pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
|
||||
}
|
||||
else {
|
||||
var maxLine = this.doc.lineCount();
|
||||
if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
|
||||
pos = Pos(pos.line + 1, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
from: function() {if (this.atOccurrence) return this.pos.from;},
|
||||
to: function() {if (this.atOccurrence) return this.pos.to;},
|
||||
|
||||
replace: function(newText, origin) {
|
||||
if (!this.atOccurrence) return;
|
||||
var lines = CodeMirror.splitLines(newText);
|
||||
this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
|
||||
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
|
||||
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
|
||||
}
|
||||
};
|
||||
|
||||
// Maps a position in a case-folded line back to a position in the original line
|
||||
// (compensating for codepoints increasing in number during folding)
|
||||
function adjustPos(orig, folded, pos) {
|
||||
if (orig.length == folded.length) return pos;
|
||||
for (var pos1 = Math.min(pos, orig.length);;) {
|
||||
var len1 = orig.slice(0, pos1).toLowerCase().length;
|
||||
if (len1 < pos) ++pos1;
|
||||
else if (len1 > pos) --pos1;
|
||||
else return pos1;
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
|
||||
return new SearchCursor(this.doc, query, pos, caseFold);
|
||||
});
|
||||
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
|
||||
return new SearchCursor(this, query, pos, caseFold);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
|
||||
var ranges = [];
|
||||
var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
|
||||
while (cur.findNext()) {
|
||||
if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
|
||||
ranges.push({anchor: cur.from(), head: cur.to()});
|
||||
}
|
||||
if (ranges.length)
|
||||
this.setSelections(ranges, 0);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user