Compare commits
1 Commits
master
...
fix-empty-
Author | SHA1 | Date | |
---|---|---|---|
|
a01645daf0 |
|
@ -1,2 +1,4 @@
|
|||
vendor/
|
||||
vendor-overwrites/
|
||||
vendor-overwrites/*
|
||||
!vendor-overwrites/colorpicker
|
||||
!vendor-overwrites/csslint
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2017
|
||||
ecmaVersion: 2015
|
||||
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
webextensions: true
|
||||
|
||||
globals:
|
||||
require: readonly # in polyfill.js
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
array-bracket-spacing: [2, never]
|
||||
|
@ -22,7 +19,7 @@ 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]
|
||||
|
@ -34,7 +31,7 @@ rules:
|
|||
dot-location: [2, property]
|
||||
dot-notation: [0]
|
||||
eol-last: [2]
|
||||
eqeqeq: [1, smart]
|
||||
eqeqeq: [1, always]
|
||||
func-call-spacing: [2, never]
|
||||
func-name-matching: [0]
|
||||
func-names: [0]
|
||||
|
@ -45,15 +42,7 @@ rules:
|
|||
id-blacklist: [0]
|
||||
id-length: [0]
|
||||
id-match: [0]
|
||||
indent: [2, 2, {
|
||||
SwitchCase: 1,
|
||||
ignoreComments: true,
|
||||
ignoredNodes: [
|
||||
"TemplateLiteral > *",
|
||||
"ConditionalExpression",
|
||||
"ForStatement"
|
||||
]
|
||||
}]
|
||||
indent-legacy: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
|
||||
jsx-quotes: [0]
|
||||
key-spacing: [0]
|
||||
keyword-spacing: [2]
|
||||
|
@ -95,9 +84,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]
|
||||
|
@ -147,9 +136,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]
|
||||
|
@ -177,7 +163,7 @@ rules:
|
|||
no-unreachable: [2]
|
||||
no-unsafe-finally: [2]
|
||||
no-unsafe-negation: [2]
|
||||
no-unused-expressions: [2]
|
||||
no-unused-expressions: [1]
|
||||
no-unused-labels: [0]
|
||||
no-unused-vars: [2, {args: after-used}]
|
||||
no-use-before-define: [2, nofunc]
|
||||
|
@ -203,7 +189,7 @@ rules:
|
|||
prefer-const: [1, {destructuring: all, 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}]
|
||||
|
@ -225,16 +211,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
|
23
.github/CONTRIBUTING.md
vendored
23
.github/CONTRIBUTING.md
vendored
|
@ -24,9 +24,6 @@ If not, then provide details describing which page the feature will effect, e.g.
|
|||
## 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
|
||||
|
||||
|
@ -37,9 +34,25 @@ https://developer.chrome.com/docs/webstore/i18n/#localeTable
|
|||
* 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
|
||||
## Scripts
|
||||
|
||||
See [BUILD.md](../BUILD.md) for more information.
|
||||
* `npm run lint` - Run ESLint on all JavaScript files.
|
||||
* `npm run update` - Runs update-node & update-main scripts.
|
||||
* `npm run update-quick` - Updates development dependencies (uses `npm update`; does not include new dependencies).
|
||||
* `npm run update-locales` (admin only)- Updates locale files from Transifex. See the [updating locale files section](#updating-locale-files-admin-only) for more details.
|
||||
* `npm run update-main` - Runs update-versions & update-codemirror.
|
||||
* `npm run update-node` - Update development dependencies, removes & reinstalls `node_modules` folder (slow).
|
||||
* `npm run update-transifex` (admin only) - Upload `en/messages.json` source to Transifex.
|
||||
* `npm run update-vendor` - Update codemirror, codemirror themes & other vendor libraries.
|
||||
* `npm run update-versions` - Update version of `manifest.json` to match `package.json`.
|
||||
* `npm run zip` - Run update-versions, then compress required files into a zip file.
|
||||
|
||||
## Updating locale files (admin only)
|
||||
|
||||
* Make sure you have the Transifex client installed. Follow the instructions on [this page](https://docs.transifex.com/client/installing-the-client).
|
||||
* Contact another admin if you need the `.transifexrc` file in the root folder. It includes the API key to use Transifex's API.
|
||||
* Use `npm run update-locales` in the command line to [update the language files](https://docs.transifex.com/client/pull) in the repo.
|
||||
* Use `npm run update-transifex` in the command line to [upload the source](https://docs.transifex.com/client/push) `en/messages.json` file to Transifex.
|
||||
|
||||
## Contact us
|
||||
|
||||
|
|
9
.github/ISSUE_TEMPLATE.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
* **Browser**:
|
||||
* **Operating System**:
|
||||
* **Stylus Version**:
|
||||
* **Screenshot**:
|
||||
|
||||
<!--
|
||||
Please make sure you checked that your issue wasn't already addressed.
|
||||
If the issue persists, please help us identifying the cause by providing the above details.
|
||||
-->
|
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Create a report about a bug you experienced while using Stylus.
|
||||
title: "[Bug] Replace with title"
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
⚠⚠ Do not delete this issue template! ⚠⚠
|
||||
Reported issues must use this template and have all the necessary information provided.
|
||||
Incomplete reports are likely to be ignored and closed.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Thank you for taking the time to create a report about a bug.
|
||||
Ensure that there are no other existing reports for this bug.
|
||||
Please check if the issue is resolved after a restart of the browser.
|
||||
Additionally, you should check if the issue persists in a new browser profile.
|
||||
Remember to fill out every section on this report and remove any that are not needed.
|
||||
Finally, place a brief description in the title of this report.
|
||||
-->
|
||||
|
||||
# Bug Report
|
||||
|
||||
### Bug Description
|
||||
<!-- Provide a clear and concise description, which will allow us to properly troubleshoot this bug. -->
|
||||
|
||||
### Screenshots
|
||||
<!-- If applicable, add screenshots to help explain this bug. -->
|
||||
|
||||
### CSS Code
|
||||
<!--
|
||||
If the bug is related to (user)CSS or the editor,
|
||||
please post the code (with a service like pastebin) in this bug report.
|
||||
-->
|
||||
|
||||
### System Information
|
||||
<!--
|
||||
Specify the browser name and version as well as the Stylus version you are using.
|
||||
Please do an online search for help if you are not familiar with how to get this information.
|
||||
-->
|
||||
|
||||
- OS: <!-- e.g. Windows, macOS, Linux -->
|
||||
- Browser: <!-- e.g. Chrome 91, Firefox 90, Edge 91, Safari 14 -->
|
||||
- Stylus Version: <!-- e.g. 1.5.21 -->
|
||||
|
||||
### Additional Context
|
||||
<!-- Provide any additional information about this bug. -->
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -1,14 +0,0 @@
|
|||
name: ci
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: npm install
|
||||
- run: npm test
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,8 +1,9 @@
|
|||
*.zip
|
||||
.DS_Store
|
||||
pull_locales_login.rb
|
||||
.vscode
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
*.zip
|
||||
.eslintcache
|
||||
.transifexrc
|
||||
.vscode
|
||||
desktop.ini
|
||||
node_modules/
|
||||
yarn.lock
|
||||
|
|
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.
|
17
README.md
17
README.md
|
@ -21,9 +21,12 @@ Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExt
|
|||
|
||||
## Screenshots
|
||||
|
||||
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)
|
||||
![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 config for usercss](https://user-images.githubusercontent.com/1310400/34453462-218a589a-ed67-11e7-9040-7d0469eeadc3.png)
|
||||
![Popup inline search](https://user-images.githubusercontent.com/1310400/34453463-21a44368-ed67-11e7-93b2-e1c8f5aac868.png)
|
||||
![Style manager config for usercss](https://user-images.githubusercontent.com/1310400/34453464-21bdaf9c-ed67-11e7-8517-62d2f02e1918.png)
|
||||
![Options](https://user-images.githubusercontent.com/1310400/34453461-216aee4c-ed67-11e7-92db-ea21c1da5826.png)
|
||||
|
||||
## Help
|
||||
|
||||
|
@ -44,15 +47,15 @@ See our [contributing](./.github/CONTRIBUTING.md) page for more details.
|
|||
|
||||
## License
|
||||
|
||||
Inherited code from the original [Stylish](https://github.com/stylish-userstyles/stylish/):
|
||||
Inherited code from the original [Stylish](https://github.com/stylish-userstyles/stylish/):
|
||||
|
||||
Copyright © 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
|
||||
|
||||
Current Stylus:
|
||||
Current Stylus:
|
||||
|
||||
Copyright © 2017-2022 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
|
||||
Copyright © 2017-2018 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
|
||||
|
||||
**[GNU GPLv3](./LICENSE)**
|
||||
**[GNU GPLv3](./LICENSE)**
|
||||
|
||||
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
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"InaccessibleFileHint": {
|
||||
"message": "Stylus لا يستطيع الوصول الى بعض انواع الملفات ( ملفات pdf و json )"
|
||||
},
|
||||
"addStyleLabel": {
|
||||
"message": "كتابة نمط جديد"
|
||||
"message": "كتابة نمط جديد",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "إضافة نمط"
|
||||
"message": "إضافة نمط",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "إضافة"
|
||||
"message": "إضافة",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "ينطبق على: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -20,73 +21,84 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "و المزيد"
|
||||
"message": "والمزيد",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "عناوين URL في النطاق"
|
||||
"message": "عناوين URL في النطاق",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم."
|
||||
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "ينطبق على"
|
||||
"message": "ينطبق على",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "عناوين URL التي تطابق regexp"
|
||||
"message": "عناوين URL التي تطابق regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "إزالة"
|
||||
"message": "إزالة",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "تحديد"
|
||||
"message": "تحديد",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "كل شيء"
|
||||
"message": "كل شيء",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "عنوان URL"
|
||||
"message": "عنوان URL",
|
||||
"description": "Option to make the style apply to the entered string as a URL"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "عناوين URL البادئة بـ"
|
||||
"message": "عناوين URL البادئة بـ",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "البحث عن تحديثات لكل الأنماط"
|
||||
"message": "البحث عن تحديثات لكل الأنماط",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "البحث عن تحديث"
|
||||
"message": "البحث عن تحديث",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "جارٍ البحث..."
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "حذف"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "حفظ"
|
||||
"message": "جارٍ البحث...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "هل تريد بالتأكيد حذف هذا النمط؟"
|
||||
"message": "هل تريد بالتأكيد حذف هذا النمط؟",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "حذف"
|
||||
"message": "حذف",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"description": {
|
||||
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى."
|
||||
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "تعطيل"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "حذف"
|
||||
"message": "تعطيل",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "تعديل النمط"
|
||||
"message": "تعديل النمط",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "تعديل"
|
||||
"message": "تعديل",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "تعديل النمط $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -94,55 +106,60 @@
|
|||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "تمكين"
|
||||
"message": "تمكين",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "إضافة"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "ممكّن"
|
||||
"findStylesForSite": {
|
||||
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "مساعدة"
|
||||
"message": "مساعدة",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "تثبيت التحديث"
|
||||
"message": "تثبيت التحديث",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "الأنماط المثبتة"
|
||||
"message": "الأنماط المثبتة",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا."
|
||||
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "إدارة الأنماط المثبتة"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "عنوان URL"
|
||||
"message": "إدارة الأنماط المثبتة",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "إضافة قسم آخر"
|
||||
"message": "إضافة قسم آخر",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "الرمز"
|
||||
"message": "الرمز",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "إزالة القسم"
|
||||
},
|
||||
"sections": {
|
||||
"message": "الأقسام"
|
||||
"message": "إزالة القسم",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "رجوع للإدارة"
|
||||
"message": "رجوع للإدارة",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها."
|
||||
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "ممكّن"
|
||||
"message": "ممكّن",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -150,16 +167,20 @@
|
|||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "أدخل اسمًا"
|
||||
"message": "أدخل اسمًا",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "حفظ"
|
||||
"message": "حفظ",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org."
|
||||
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
|
@ -167,12 +188,15 @@
|
|||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه."
|
||||
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "النمط محدّث."
|
||||
"message": "النمط محدّث.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "اكتمل التحديث."
|
||||
"message": "اكتمل التحديث.",
|
||||
"description": "Text that displays when an update completed"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Писане на нов стил"
|
||||
"message": "Писане на нов стил",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Добавяне на стил"
|
||||
"message": "Добавяне на стил",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Добавяне"
|
||||
"message": "Добавяне",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Приложимо за: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -17,240 +21,317 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "и още"
|
||||
"message": "и още",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "Адреси на домейна"
|
||||
"message": "Адреси на домейна",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела."
|
||||
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Приложимо за"
|
||||
"message": "Приложимо за",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "Адреси, съвпадащи с регулярен израз"
|
||||
"message": "Адреси, съвпадащи с регулярен израз",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Премахване"
|
||||
"message": "Премахване",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Уточняване"
|
||||
"message": "Уточняване",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Всичко"
|
||||
"message": "Всичко",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "Адрес"
|
||||
"message": "Адрес",
|
||||
"description": "Option to make the style apply to the entered string as a URL"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "Адреси, започващи с"
|
||||
"message": "Адреси, започващи с",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Прилагане на всички обновления"
|
||||
"message": "Прилагане на всички обновления",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Резервни копия"
|
||||
"message": "Резервни копия",
|
||||
"description": "Heading for backup"
|
||||
},
|
||||
"backupMessage": {
|
||||
"message": "Изберете файл или го влачете до страницата.",
|
||||
"description": "Message for backup"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Изнасяне на стилове"
|
||||
"message": "Изнасяне на стилове",
|
||||
"description": ""
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Проверка на всички стилове за обновления"
|
||||
"message": "Проверка на всички стилове за обновления",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Повторна проверка"
|
||||
"message": "Повторна проверка",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Проверка за обновления"
|
||||
"message": "Проверка за обновления",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Проверяване..."
|
||||
"message": "Проверяване...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"cm_autocompleteOnTyping": {
|
||||
"message": "Автоматично завършване при въвеждане"
|
||||
"message": "Автоматично завършване при въвеждане",
|
||||
"description": "Label for the checkbox in the style editor."
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Подпрозорци с умен отстъп"
|
||||
"message": "Подпрозорци с умен отстъп",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Клавиши"
|
||||
"message": "Клавиши",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Пренасяне"
|
||||
"message": "Пренасяне",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"cm_matchHighlight": {
|
||||
"message": "Осветяване"
|
||||
"message": "Осветяване",
|
||||
"description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor."
|
||||
},
|
||||
"cm_matchHighlightSelection": {
|
||||
"message": "Само избраното"
|
||||
"message": "Само избраното",
|
||||
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
|
||||
},
|
||||
"cm_matchHighlightToken": {
|
||||
"message": "Низа под показалеца"
|
||||
"message": "Низа под показалеца",
|
||||
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of the word/token under cursor even if nothing is selected"
|
||||
},
|
||||
"cm_resizeGripHint": {
|
||||
"message": "Щракнете два пъти за възстановяване/увеличаване на височината"
|
||||
"message": "Щракнете два пъти за възстановяване/увеличаване на височината",
|
||||
"description": "Tooltip for the resize grip in style editor"
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Умен отстъп"
|
||||
"message": "Умен отстъп",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Табулация"
|
||||
"message": "Табулация",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Тема"
|
||||
"message": "Тема",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Отказ"
|
||||
"message": "Отказ",
|
||||
"description": ""
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Изтриване"
|
||||
"message": "Изтриване",
|
||||
"description": ""
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Не"
|
||||
"message": "Не",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"confirmOK": {
|
||||
"message": "Добре"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Запазване"
|
||||
"message": "Добре",
|
||||
"description": ""
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Спиране"
|
||||
"message": "Спиране",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Да"
|
||||
"message": "Да",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?"
|
||||
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "по подразбиране"
|
||||
"message": "по подразбиране",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Сигурни ли сте, че искате да изтриете стила?"
|
||||
"message": "Сигурни ли сте, че искате да изтриете стила?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Изтриване"
|
||||
"message": "Изтриване",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"description": {
|
||||
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове."
|
||||
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Изключване на всички стилове"
|
||||
"message": "Изключване на всички стилове",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Изключване"
|
||||
"message": "Изключване",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"dragDropMessage": {
|
||||
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете."
|
||||
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете.",
|
||||
"description": "Drag'n'drop message"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Изтриване"
|
||||
"message": "Изтриване",
|
||||
"description": "Label for the context menu item in the editor to delete selected text"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Отиване на ред"
|
||||
"message": "Отиване на ред",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Редактиране на стила"
|
||||
"message": "Редактиране на стила",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Редактиране"
|
||||
"message": "Редактиране",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Редактиране на стила $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Стилове за редактора",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Включване"
|
||||
"message": "Включване",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Изнасяне"
|
||||
"message": "Изнасяне",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Добавяне"
|
||||
"findStylesForSite": {
|
||||
"message": "Още стилове за този сайт",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Изключено"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Включено"
|
||||
"message": "Изключено",
|
||||
"description": "Used in various lists/options to indicate that something is disabled"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "Хронология"
|
||||
"message": "Хронология",
|
||||
"description": "Used in various places to show a history log of something"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Помощ"
|
||||
"message": "Помощ",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Въведете име"
|
||||
"message": "Въведете име",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Натиснете клавиш"
|
||||
"message": "Натиснете клавиш",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Прибавяне към стила"
|
||||
"message": "Прибавяне към стила",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Прибавяне на внесения стил към текущия"
|
||||
"message": "Прибавяне на внесения стил към текущия",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Внасяне"
|
||||
"message": "Внасяне",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Презаписване на стила"
|
||||
"message": "Презаписване на стила",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Презаписване на съдържанието на текщия стил с това от внесения"
|
||||
"message": "Презаписване на съдържанието на текщия стил с това от внесения",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"importReportLegendAdded": {
|
||||
"message": "добавени"
|
||||
"message": "добавени",
|
||||
"description": "Text after the number of styles added in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendIdentical": {
|
||||
"message": "пропуснати еднакви"
|
||||
"message": "пропуснати еднакви",
|
||||
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendInvalid": {
|
||||
"message": "пропуснати невалидни"
|
||||
"message": "пропуснати невалидни",
|
||||
"description": "Text after the number of styles skipped due to being invalid (not a Stylus/Stylish backup file probably) in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendUpdatedBoth": {
|
||||
"message": "с обновени код и метаданни"
|
||||
"message": "с обновени код и метаданни",
|
||||
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendUpdatedCode": {
|
||||
"message": "с обновен код"
|
||||
"message": "с обновен код",
|
||||
"description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendUpdatedMeta": {
|
||||
"message": "с обновени метаданни"
|
||||
"message": "с обновени метаданни",
|
||||
"description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Внасянето на стилове завърши"
|
||||
"message": "Внасянето на стилове завърши",
|
||||
"description": "Title of the report shown after importing styles"
|
||||
},
|
||||
"importReportUnchanged": {
|
||||
"message": "Нищо не беше променено."
|
||||
"message": "Нищо не беше променено.",
|
||||
"description": "Message in the report shown after importing styles"
|
||||
},
|
||||
"importReportUndone": {
|
||||
"message": "върнати стила"
|
||||
"message": "върнати стила",
|
||||
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
|
||||
},
|
||||
"importReportUndoneTitle": {
|
||||
"message": "Внасянето беше отменено"
|
||||
"message": "Внасянето беше отменено",
|
||||
"description": "Title of the message box shown after undoing the import of styles"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Инсталиране на обновлението"
|
||||
"message": "Инсталиране на обновлението",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Помощ"
|
||||
"message": "Помощ",
|
||||
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Вземете стилове"
|
||||
"message": "Вземете стилове",
|
||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Проблеми"
|
||||
"message": "Проблеми",
|
||||
"description": "Label for the CSS linter issues block on the style edit page"
|
||||
},
|
||||
"linterIssuesHelp": {
|
||||
"message": "Проблеми, намерени от $link$ при следните правила:",
|
||||
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"content": "$1"
|
||||
|
@ -258,187 +339,252 @@
|
|||
}
|
||||
},
|
||||
"manageFavicons": {
|
||||
"message": "Иконки на приложимите сайтовете"
|
||||
"message": "Иконки на приложимите сайтовете",
|
||||
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"manageFaviconsGray": {
|
||||
"message": "Сиви"
|
||||
"message": "Сиви",
|
||||
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"manageFaviconsHelp": {
|
||||
"message": "Разширението използва външна услуга https://www.google.com/s2/favicons",
|
||||
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Филтри"
|
||||
"message": "Филтри",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Инсталирани стилове"
|
||||
"message": "Инсталирани стилове",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"manageMaxTargets": {
|
||||
"message": "Брой на видимите приложими адреси"
|
||||
"message": "Брой на видимите приложими адреси",
|
||||
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "Нов интерфейс за управление"
|
||||
"message": "Нов интерфейс за управление",
|
||||
"description": "Label for the checkbox that toggles the new UI on manage page"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Само включените стилове"
|
||||
"message": "Само включените стилове",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"manageOnlyLocal": {
|
||||
"message": "Само местно създадените стилове"
|
||||
"message": "Само местно създадените стилове",
|
||||
"description": "Checkbox to show only locally created styles i.e. non-updatable"
|
||||
},
|
||||
"manageOnlyLocalTooltip": {
|
||||
"message": "(стиловете, които не са инсталирани през userstyles.org)"
|
||||
"message": "(стиловете, които не са инсталирани през userstyles.org)",
|
||||
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
|
||||
},
|
||||
"manageOnlyUpdates": {
|
||||
"message": "Само стилове с обновления или проблеми"
|
||||
"message": "Само стилове с обновления или проблеми",
|
||||
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Стилове"
|
||||
"message": "Стилове",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Брой на активните стилове"
|
||||
"message": "Брой на активните стилове",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Няма инсталирани стилове за сайта."
|
||||
"message": "Няма инсталирани стилове за сайта.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Управление"
|
||||
"message": "Управление",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Настройки"
|
||||
"openOptionsManage": {
|
||||
"message": "Прозорец за настройките",
|
||||
"description": "Go to Options UI"
|
||||
},
|
||||
"openOptionsPopup": {
|
||||
"message": "Настройки",
|
||||
"description": "Go to Options UI"
|
||||
},
|
||||
"openStylesManager": {
|
||||
"message": "Управление на стиловете"
|
||||
"message": "Управление на стиловете",
|
||||
"description": "Label for the style maanger opener in the browser action context menu."
|
||||
},
|
||||
"optionsActions": {
|
||||
"message": "Действия"
|
||||
"message": "Действия",
|
||||
"description": ""
|
||||
},
|
||||
"optionsAdvanced": {
|
||||
"message": "Разширени"
|
||||
"message": "Разширени",
|
||||
"description": ""
|
||||
},
|
||||
"optionsAdvancedContextDelete": {
|
||||
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора"
|
||||
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора",
|
||||
"description": ""
|
||||
},
|
||||
"optionsAdvancedExposeIframes": {
|
||||
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]"
|
||||
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]",
|
||||
"description": ""
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Цвят на фона, когато е изключено"
|
||||
"message": "Цвят на фона, когато е изключено",
|
||||
"description": ""
|
||||
},
|
||||
"optionsBadgeNormal": {
|
||||
"message": "Цвят на фона"
|
||||
"message": "Цвят на фона",
|
||||
"description": ""
|
||||
},
|
||||
"optionsCheck": {
|
||||
"message": "Обновяване на стиловете"
|
||||
"message": "Обновяване на стиловете",
|
||||
"description": ""
|
||||
},
|
||||
"optionsCheckUpdate": {
|
||||
"message": "Проверка и инсталиране на наличните обновления"
|
||||
"message": "Проверка и инсталиране на наличните обновления",
|
||||
"description": ""
|
||||
},
|
||||
"optionsCustomizeBadge": {
|
||||
"message": "Значка на иконката на лентата"
|
||||
"message": "Значка на иконката на лентата",
|
||||
"description": ""
|
||||
},
|
||||
"optionsCustomizeIcon": {
|
||||
"message": "Иконка на лентата със сечива"
|
||||
"message": "Иконка на лентата със сечива",
|
||||
"description": ""
|
||||
},
|
||||
"optionsCustomizePopup": {
|
||||
"message": "Падащ прозорец"
|
||||
"message": "Падащ прозорец",
|
||||
"description": ""
|
||||
},
|
||||
"optionsCustomizeUpdate": {
|
||||
"message": "Обновления"
|
||||
"message": "Обновления",
|
||||
"description": ""
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Настройки"
|
||||
"message": "Настройки",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"optionsIconDark": {
|
||||
"message": "Тъмни теми"
|
||||
"message": "Тъмни теми",
|
||||
"description": ""
|
||||
},
|
||||
"optionsIconLight": {
|
||||
"message": "Светли теми"
|
||||
"message": "Светли теми",
|
||||
"description": ""
|
||||
},
|
||||
"optionsOpen": {
|
||||
"message": "Отваряне"
|
||||
"message": "Отваряне",
|
||||
"description": ""
|
||||
},
|
||||
"optionsOpenManager": {
|
||||
"message": "Управление на стиловете"
|
||||
"message": "Управление на стиловете",
|
||||
"description": ""
|
||||
},
|
||||
"optionsPopupWidth": {
|
||||
"message": "Ширина на падащия прозорец (в пиксели)"
|
||||
"message": "Ширина на падащия прозорец (в пиксели)",
|
||||
"description": ""
|
||||
},
|
||||
"optionsReset": {
|
||||
"message": "Зануляване на настройки на първоначалните стойности"
|
||||
"message": "Зануляване на настройки на първоначалните стойности",
|
||||
"description": ""
|
||||
},
|
||||
"optionsResetButton": {
|
||||
"message": "Зануляване на настройките"
|
||||
"message": "Зануляване на настройките",
|
||||
"description": ""
|
||||
},
|
||||
"optionsSubheading": {
|
||||
"message": "Още настройки"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "Адрес"
|
||||
"message": "Още настройки",
|
||||
"description": "Subheading for options section on manage page."
|
||||
},
|
||||
"optionsUpdateImportNote": {
|
||||
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални."
|
||||
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални.",
|
||||
"description": ""
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "Стилове преди командите"
|
||||
"message": "Стилове преди командите",
|
||||
"description": "Label for the checkbox controlling section order in the popup."
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Брой на активните стилове за текущия сайт"
|
||||
"message": "Брой на активните стилове за текущия сайт",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"replace": {
|
||||
"message": "Заместване"
|
||||
"message": "Заместване",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Заместване на всички"
|
||||
"message": "Заместване на всички",
|
||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Заместване с"
|
||||
"message": "Заместване с",
|
||||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||
},
|
||||
"retrieveBckp": {
|
||||
"message": "Внасяне на стилове"
|
||||
"message": "Внасяне на стилове",
|
||||
"description": ""
|
||||
},
|
||||
"search": {
|
||||
"message": "Търсене"
|
||||
"message": "Търсене",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази"
|
||||
"message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Търсене на съдържанието",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Добавяне на друг отдел"
|
||||
"message": "Добавяне на друг отдел",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Код"
|
||||
"message": "Код",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Премахване на отдела"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Отдели"
|
||||
"message": "Премахване на отдела",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"shortcuts": {
|
||||
"message": "Клавишни комбинации"
|
||||
"message": "Клавишни комбинации",
|
||||
"description": "Go to shortcut configuration"
|
||||
},
|
||||
"shortcutsNote": {
|
||||
"message": "Задаване на клавишни комбинации"
|
||||
"message": "Задаване на клавишни комбинации",
|
||||
"description": ""
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Регулярният израз не е правилен."
|
||||
"message": "Регулярният израз не е правилен.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Разкрасяване"
|
||||
"message": "Разкрасяване",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleBeautifyIndentConditional": {
|
||||
"message": "Отстъп на @media, @supports"
|
||||
"message": "Отстъп на @media, @supports",
|
||||
"description": "CSS-beautifier option"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Назад към стиловете"
|
||||
"message": "Назад към стиловете",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Направили сте промени по стила без да ги запазите."
|
||||
"message": "Направили сте промени по стила без да ги запазите.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Включено"
|
||||
"message": "Включено",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Поставете кода във формат на Мозила"
|
||||
"message": "Поставете кода във формат на Мозила",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Да се инсталира ли '$stylename$'?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -446,49 +592,68 @@
|
|||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Въведете име"
|
||||
"message": "Въведете име",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Формат на Мозила"
|
||||
"message": "Формат на Мозила",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"styleNotAppliedRegexpProblemTooltip": {
|
||||
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази"
|
||||
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази",
|
||||
"description": "Tooltip in the popup for styles that were not applied at all"
|
||||
},
|
||||
"styleRegexpInvalidExplanation": {
|
||||
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани."
|
||||
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани.",
|
||||
"description": ""
|
||||
},
|
||||
"styleRegexpPartialExplanation": {
|
||||
"message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект)."
|
||||
"message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект).",
|
||||
"description": ""
|
||||
},
|
||||
"styleRegexpProblemTooltip": {
|
||||
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази"
|
||||
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази",
|
||||
"description": "Tooltip in the popup for styles that were applied only partially"
|
||||
},
|
||||
"styleRegexpTestButton": {
|
||||
"message": "Тест на регулярния израз",
|
||||
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
|
||||
},
|
||||
"styleRegexpTestFull": {
|
||||
"message": "Съвпадащи подпрозорци"
|
||||
"message": "Съвпадащи подпрозорци",
|
||||
"description": "RegExp test report: label for the fully matching expressions"
|
||||
},
|
||||
"styleRegexpTestInvalid": {
|
||||
"message": "Неправилните регулярни изрази са пропуснати"
|
||||
"message": "Неправилните регулярни изрази са пропуснати",
|
||||
"description": "RegExp test report: label for the invalid expressions"
|
||||
},
|
||||
"styleRegexpTestNone": {
|
||||
"message": "Няма съвпадащи подпрозорци"
|
||||
"message": "Няма съвпадащи подпрозорци",
|
||||
"description": "RegExp test report: label for expressions that didn't match any tabs"
|
||||
},
|
||||
"styleRegexpTestPartial": {
|
||||
"message": "Не съвпада напълно, затова е пропуснато"
|
||||
"message": "Не съвпада напълно, затова е пропуснато",
|
||||
"description": "RegExp test report: label for the partially matching expressions"
|
||||
},
|
||||
"styleRegexpTestTitle": {
|
||||
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)"
|
||||
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)",
|
||||
"description": "RegExp test report: title of the report"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Запазване"
|
||||
"message": "Запазване",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)"
|
||||
"message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Стил във формат на Мозила"
|
||||
"message": "Стил във формат на Мозила",
|
||||
"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$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -496,34 +661,44 @@
|
|||
}
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Разширението не работи на такива страници."
|
||||
"message": "Разширението не работи на такива страници.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"stylusUnavailableForURLdetails": {
|
||||
"message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен."
|
||||
"message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен.",
|
||||
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Превключване на стила"
|
||||
"message": "Превключване на стила",
|
||||
"description": "Label for the checkbox to enable/disable a style"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Отмяна"
|
||||
"message": "Отмяна",
|
||||
"description": "Button label"
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Отмяна във всички отдели"
|
||||
"message": "Отмяна във всички отдели",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"unreachableContentScript": {
|
||||
"message": "Няма връзка със страницата. Презаредете подпрозореца."
|
||||
"message": "Няма връзка със страницата. Презаредете подпрозореца.",
|
||||
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
|
||||
},
|
||||
"unreachableFileHint": {
|
||||
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions."
|
||||
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions.",
|
||||
"description": "Note in the toolbar popup for file:// URLs"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Няма намерени обновления."
|
||||
"message": "Няма намерени обновления.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateAllCheckSucceededSomeEdited": {
|
||||
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани)."
|
||||
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани).",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Неуспешно обновяване: сървърът отговори с код $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
|
@ -531,36 +706,47 @@
|
|||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Неуспешно обновяване: няма връзка със сървъра."
|
||||
"message": "Неуспешно обновяване: няма връзка със сървъра.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckHistory": {
|
||||
"message": "Хронология на проверките"
|
||||
"message": "Хронология на проверките",
|
||||
"description": ""
|
||||
},
|
||||
"updateCheckManualUpdateForce": {
|
||||
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)"
|
||||
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)",
|
||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckManualUpdateHint": {
|
||||
"message": "Принудителното обновяване ще презапише местните редакции."
|
||||
"message": "Принудителното обновяване ще презапише местните редакции.",
|
||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckSkippedLocallyEdited": {
|
||||
"message": "Стилът е бил местно редактиран."
|
||||
"message": "Стилът е бил местно редактиран.",
|
||||
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckSkippedMaybeLocallyEdited": {
|
||||
"message": "Стилът може да е бил местно редактиран."
|
||||
"message": "Стилът може да е бил местно редактиран.",
|
||||
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Стилът е обновен."
|
||||
"message": "Стилът е обновен.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Обновяването е завършено."
|
||||
"message": "Обновяването е завършено.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"updatesCurrentlyInstalled": {
|
||||
"message": "Инсталирани обновления:"
|
||||
"message": "Инсталирани обновления:",
|
||||
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Писане на стил за: "
|
||||
"message": "Писане на стил за: ",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "този адрес"
|
||||
"message": "този адрес",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Напиши нов стил"
|
||||
"message": "Напиши нов стил",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Добави стил"
|
||||
"message": "Добави стил",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Добави"
|
||||
"message": "Добави",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Прилага се към: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -17,103 +21,136 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "и още"
|
||||
"message": "и още",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URLи на домейна"
|
||||
"message": "URLи на домейна",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция."
|
||||
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Прилага се към"
|
||||
"message": "Прилага се към",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "Адреси, съвпадащи с regexp"
|
||||
"message": "Адреси, съвпадащи с regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Премахни"
|
||||
"message": "Премахни",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Уточни"
|
||||
"message": "Уточни",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Всички"
|
||||
"message": "Всички",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL започващи с"
|
||||
"message": "URL започващи с",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Приложи всички промени"
|
||||
"message": "Приложи всички промени",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Провери всички стилове за обновления"
|
||||
"message": "Провери всички стилове за обновления",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Провери за обновление"
|
||||
"message": "Провери за обновление",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Проверявам..."
|
||||
"message": "Проверявам...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Използвай табулация с умно отместване"
|
||||
"message": "Използвай табулация с умно отместване",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Клавишни комбинации"
|
||||
"message": "Клавишни комбинации",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Автоматично пренасяне"
|
||||
"message": "Автоматично пренасяне",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Използвай умно отместване"
|
||||
"message": "Използвай умно отместване",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Размер на табулацията"
|
||||
"message": "Размер на табулацията",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Тема"
|
||||
"message": "Тема",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Не"
|
||||
"message": "Не",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Спри"
|
||||
"message": "Спри",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Да"
|
||||
"message": "Да",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?"
|
||||
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "по подразбиране"
|
||||
"message": "по подразбиране",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Наистина ли искаш да изтриеш този стил?"
|
||||
"message": "Наистина ли искаш да изтриеш този стил?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Изтрий"
|
||||
"message": "Изтрий",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"description": {
|
||||
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове."
|
||||
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Изключи всички стилове"
|
||||
"message": "Изключи всички стилове",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Забрани"
|
||||
"message": "Забрани",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Иди на ред (или ред:кол)"
|
||||
"message": "Иди на ред (или ред:кол)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Промени стила"
|
||||
"message": "Промени стила",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Редактирай"
|
||||
"message": "Редактирай",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Редактирай стил $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -121,52 +158,68 @@
|
|||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Разреши"
|
||||
"message": "Разреши",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Експорт"
|
||||
"message": "Експорт",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Помощ"
|
||||
"message": "Помощ",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Напиши име на команда"
|
||||
"message": "Напиши име на команда",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Натисни клавишна комбинация"
|
||||
"message": "Натисни клавишна комбинация",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Добави към стил"
|
||||
"message": "Добави към стил",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Добави импортирания стил към текущия"
|
||||
"message": "Добави импортирания стил към текущия",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Импорт"
|
||||
"message": "Импорт",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Презапиши стила"
|
||||
"message": "Презапиши стила",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Презапишете съдържанието на текущия стил с импортирания"
|
||||
"message": "Презапишете съдържанието на текущия стил с импортирания",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"installButton": {
|
||||
"message": "Инсталирай стил"
|
||||
"message": "Инсталирай стил",
|
||||
"description": "Label for install button"
|
||||
},
|
||||
"installButtonInstalled": {
|
||||
"message": "Стилът е инсталиран"
|
||||
"message": "Стилът е инсталиран",
|
||||
"description": "Text displayed when the style is successfully installed"
|
||||
},
|
||||
"installButtonReinstall": {
|
||||
"message": "Преинсталирай стила"
|
||||
"message": "Преинсталирай стила",
|
||||
"description": "Label for reinstall button"
|
||||
},
|
||||
"installButtonUpdate": {
|
||||
"message": "Обнови стила"
|
||||
"message": "Обнови стила",
|
||||
"description": "Label for update button"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Инсталирай обновление"
|
||||
"message": "Инсталирай обновление",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"installUpdateFrom": {
|
||||
"message": "В момента стилът се обновява от $url$",
|
||||
"description": "Label to describe where the style gets update",
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"content": "$1"
|
||||
|
@ -174,22 +227,32 @@
|
|||
}
|
||||
},
|
||||
"installUpdateFromLabel": {
|
||||
"message": "Провери за обновления"
|
||||
"message": "Провери за обновления",
|
||||
"description": "Label for the checkbox to save current URL for update check"
|
||||
},
|
||||
"installUpdateUnavailable": {
|
||||
"message": "За да разрешите проверка за обновления, пуснете файла върху лентата с табове, или в метаданните на стила укажете @updateURL.",
|
||||
"description": ""
|
||||
},
|
||||
"license": {
|
||||
"message": "Лиценз"
|
||||
"message": "Лиценз",
|
||||
"description": "Label for the license"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Получете помощ"
|
||||
"message": "Получете помощ",
|
||||
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Вземете стилове"
|
||||
"message": "Вземете стилове",
|
||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
||||
},
|
||||
"linkTranslate": {
|
||||
"message": "Преведете"
|
||||
"message": "Преведете",
|
||||
"description": "Transifex link text on the manage page"
|
||||
},
|
||||
"linterCSSLintIncompatible": {
|
||||
"message": "CSSLint не поддържа $preprocessorname$ preprocessor",
|
||||
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
|
||||
"placeholders": {
|
||||
"preprocessorname": {
|
||||
"content": "$1"
|
||||
|
@ -197,10 +260,12 @@
|
|||
}
|
||||
},
|
||||
"linterCSSLintSettings": {
|
||||
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)"
|
||||
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)",
|
||||
"description": "CSSLint rule config values"
|
||||
},
|
||||
"linterConfigPopupTitle": {
|
||||
"message": "Настройте конфигурация за $linter$ правила",
|
||||
"description": "Stylelint or CSSLint popup header",
|
||||
"placeholders": {
|
||||
"linter": {
|
||||
"content": "$1"
|
||||
|
@ -208,16 +273,20 @@
|
|||
}
|
||||
},
|
||||
"linterConfigTooltip": {
|
||||
"message": "Щракнете, за да конфигурирате този linter"
|
||||
"message": "Щракнете, за да конфигурирате този linter",
|
||||
"description": "Icon tooltip to indicate that it opens a popup with the selected linter configuration"
|
||||
},
|
||||
"linterInvalidConfigError": {
|
||||
"message": "Не е записано заради тези неправилни настройки"
|
||||
"message": "Не е записано заради тези неправилни настройки",
|
||||
"description": "Invalid linter config will show a message followed by a list of invalid entries"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Проблеми"
|
||||
"message": "Проблеми",
|
||||
"description": "Label for the CSS linter issues block on the style edit page"
|
||||
},
|
||||
"linterIssuesHelp": {
|
||||
"message": "Тези проблеми бяха намерени от $link$:",
|
||||
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"content": "$1"
|
||||
|
@ -225,54 +294,79 @@
|
|||
}
|
||||
},
|
||||
"linterJSONError": {
|
||||
"message": "Невалиден JSON формат"
|
||||
"message": "Невалиден JSON формат",
|
||||
"description": "Setting linter config with invalid JSON"
|
||||
},
|
||||
"linterResetMessage": {
|
||||
"message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец"
|
||||
"message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец",
|
||||
"description": "Reset button tooltip to inform user on how to undo an accidental reset"
|
||||
},
|
||||
"linterRulesLink": {
|
||||
"message": "Вижте пълния списък с правила"
|
||||
"message": "Вижте пълния списък с правила",
|
||||
"description": "Stylelint or CSSLint rules label added immediately before a link"
|
||||
},
|
||||
"liveReloadError": {
|
||||
"message": "Получи се грешка докато наблюдавахме файла"
|
||||
"message": "Получи се грешка докато наблюдавахме файла",
|
||||
"description": "The label of live-reload error"
|
||||
},
|
||||
"liveReloadInstallHint": {
|
||||
"message": "Преглед на живо е разрешен, така че инсталирания стил ще бъде обновен автоматично при външни промени докато двата прозореца с кода и оригинала са отворени.",
|
||||
"description": "The label of live-reload feature"
|
||||
},
|
||||
"liveReloadLabel": {
|
||||
"message": "Преглед на живо"
|
||||
"message": "Преглед на живо",
|
||||
"description": "The label of live-reload feature"
|
||||
},
|
||||
"liveReloadUnavailable": {
|
||||
"message": "За да разрешите презареждане в реално време, пуснете файла върху лентата с табове (областта, където са показани заглавията на табовете).",
|
||||
"description": ""
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Филтри"
|
||||
"message": "Филтри",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Инсталирани стилове"
|
||||
"message": "Инсталирани стилове",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"manageNewStyleAsUsercss": {
|
||||
"message": "като Потребителскиcss"
|
||||
"message": "като Потребителскиcss",
|
||||
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "Нова подредба на UI"
|
||||
"message": "Нова подредба на UI",
|
||||
"description": "Label for the checkbox that toggles the new UI on manage page"
|
||||
},
|
||||
"manageOnlyDisabled": {
|
||||
"message": "Само забранените стилове"
|
||||
"message": "Само забранените стилове",
|
||||
"description": "Checkbox to show only disabled styles"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Само разрешените стилове"
|
||||
"message": "Само разрешените стилове",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"manageOnlyExternal": {
|
||||
"message": "Само външните стилове"
|
||||
"message": "Само външните стилове",
|
||||
"description": "Checkbox to show only externally installed styles i.e. updatable"
|
||||
},
|
||||
"manageOnlyLocal": {
|
||||
"message": "Само локалните стилове"
|
||||
"message": "Само локалните стилове",
|
||||
"description": "Checkbox to show only locally created styles i.e. non-updatable"
|
||||
},
|
||||
"manageOnlyLocalTooltip": {
|
||||
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)"
|
||||
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)",
|
||||
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
|
||||
},
|
||||
"manageOnlyNonUsercss": {
|
||||
"message": "Само не-Потребителскитеcss стилове"
|
||||
"message": "Само не-Потребителскитеcss стилове",
|
||||
"description": "Checkbox to show only non-Usercss (standard) styles"
|
||||
},
|
||||
"manageOnlyUpdates": {
|
||||
"message": "Само с обновления или проблеми"
|
||||
"message": "Само с обновления или проблеми",
|
||||
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
|
||||
},
|
||||
"manageOnlyUsercss": {
|
||||
"message": "Само Потребителскиcss стилове"
|
||||
"message": "Само Потребителскиcss стилове",
|
||||
"description": "Checkbox to show only Usercss styles"
|
||||
}
|
||||
}
|
||||
}
|
1
_locales/ca/messages.json
Normal file
1
_locales/ca/messages.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +1,23 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Skriv ny stil"
|
||||
"message": "Skriv ny stil",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Tilføj stil"
|
||||
"message": "Tilføj stil",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Gennemsigtighed"
|
||||
"message": "Gennemsigtighed",
|
||||
"description": "Label of color's opacity"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Tilføj"
|
||||
"message": "Tilføj",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Anvendes på: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -20,81 +25,107 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "og mere"
|
||||
"message": "og mere",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL'er på domænet"
|
||||
"message": "URL'er på domænet",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på."
|
||||
"message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Anvendes på"
|
||||
"message": "Anvendes på",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesLineWidgetLabel": {
|
||||
"message": "Vis 'Anvendes på'-info"
|
||||
"message": "Vis 'Anvendes på'-info",
|
||||
"description": "Label for the checkbox to display applies-to information in the single editor"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL'er der matcher regexp'en"
|
||||
"message": "URL'er der matcher regexp'en",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Fjern"
|
||||
"message": "Fjern",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesRemoveError": {
|
||||
"message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse"
|
||||
"message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse",
|
||||
"description": "Error displayed when the last 'applies' is going to be removed"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Specificér"
|
||||
"message": "Specificér",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Alt"
|
||||
"message": "Alt",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL'er der starter med"
|
||||
"message": "URL'er der starter med",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Anvend alle opdateringer"
|
||||
"message": "Anvend alle opdateringer",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"author": {
|
||||
"message": "Forfatter"
|
||||
"message": "Forfatter",
|
||||
"description": "Label for the style author"
|
||||
},
|
||||
"backupMessage": {
|
||||
"message": "Vælg en fil eller træk og slip til denne side.",
|
||||
"description": "Message for backup"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Eksportér stil"
|
||||
"message": "Eksportér stil",
|
||||
"description": ""
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Tjek alle stiler for opdateringer"
|
||||
"message": "Tjek alle stiler for opdateringer",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Tjek igen, jeg redigerede ikke nogen stil!"
|
||||
"message": "Tjek igen, jeg redigerede ikke nogen stil!",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Tjek efter opdatering"
|
||||
"message": "Tjek efter opdatering",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Tjekker..."
|
||||
"message": "Tjekker...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"clickToUninstall": {
|
||||
"message": "Klik for at afinstallere"
|
||||
"message": "Klik for at afinstallere",
|
||||
"description": "Label for the overlay on a style thumbnail when installed via inline search in the popup"
|
||||
},
|
||||
"cm_autoCloseBrackets": {
|
||||
"message": "Luk automatisk paranteser og citationstegn"
|
||||
"message": "Luk automatisk paranteser og citationstegn",
|
||||
"description": "Label for the checkbox in the style editor."
|
||||
},
|
||||
"cm_autoCloseBracketsTooltip": {
|
||||
"message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\""
|
||||
"message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\"",
|
||||
"description": "Label for the checkbox in the style editor."
|
||||
},
|
||||
"cm_autocompleteOnTyping": {
|
||||
"message": "Autoudfyld på indtastning"
|
||||
"message": "Autoudfyld på indtastning",
|
||||
"description": "Label for the checkbox in the style editor."
|
||||
},
|
||||
"cm_colorpicker": {
|
||||
"message": "Farvevælgere for CSS-farver"
|
||||
"message": "Farvevælgere for CSS-farver",
|
||||
"description": "Label for the checkbox controlling colorpicker option for the style editor."
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Brug tabs med smart indrykning"
|
||||
"message": "Brug tabs med smart indrykning",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Tastegenveje"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Tilføj"
|
||||
"message": "Tastegenveje",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +1,19 @@
|
|||
{
|
||||
"InaccessibleFileHint": {
|
||||
"message": "Το Stylus δεν έχει πρόσβαση σε κάποια αρχεία (π.χ. τα αρχεία PDF και JSON)"
|
||||
},
|
||||
"addStyleLabel": {
|
||||
"message": "Γράψτε νέο στυλ"
|
||||
"message": "Γράψτε νέο στυλ",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Προσθήκη στυλ"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Αδιαφάνεια"
|
||||
"message": "Προσθήκη στυλ",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Προσθήκη"
|
||||
"message": "Προσθήκη",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Ισχύει για: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -23,228 +21,112 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "και πολλά άλλα"
|
||||
"message": "και πολλά άλλα",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL στον τομέα"
|
||||
"message": "URL στον τομέα",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Χρησιμοποιήστε το \"Ισχύει για\" έλεγχοι ώστε να περιοριστουν ποιες διευθύνσεις τον κώδικα σε αυτό το τμήμα να εφαρμόζονται."
|
||||
"message": "Χρησιμοποιήστε το \"Ισχύει για\" έλεγχοι ώστε να περιοριστουν ποιες διευθύνσεις τον κώδικα σε αυτό το τμήμα να εφαρμόζονται.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Ισχύει για"
|
||||
},
|
||||
"appliesLineWidgetWarning": {
|
||||
"message": "Δε λειτουργεί με minified CSS."
|
||||
"message": "Ισχύει για",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "Διευθύνσεις URL που ταιριάζουν με την κανονική έκφραση"
|
||||
"message": "Διευθύνσεις URL που ταιριάζουν με την κανονική έκφραση",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Αφαίρεση"
|
||||
"message": "Αφαίρεση",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Καθορισμός"
|
||||
"message": "Καθορισμός",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Τα πάντα"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "διεύθυνση URL"
|
||||
"message": "Τα πάντα",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "Διευθύνσεις URL που αρχίζουν με"
|
||||
"message": "Διευθύνσεις URL που αρχίζουν με",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Εφαρμογή όλων των ενημερώσεων"
|
||||
},
|
||||
"author": {
|
||||
"message": "Συντάκτης"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Δημιουργήστε αντίγραφο ασφαλείας"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Εξαγωγή στυλ"
|
||||
"message": "Εφαρμογή όλων των ενημερώσεων",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Έλεγχος όλων των στυλ για ενημερώσεις"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Ελέγξτε πάλι, δεν επεξεργάστηκα κανένα στυλ!"
|
||||
"message": "Έλεγχος όλων των στυλ για ενημερώσεις",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Έλεγχος για ενημερώσεις"
|
||||
"message": "Έλεγχος για ενημερώσεις",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Έλεγχος..."
|
||||
},
|
||||
"clickToUninstall": {
|
||||
"message": "Πατήστε για απεγκατάσταση"
|
||||
},
|
||||
"cm_autoCloseBrackets": {
|
||||
"message": "Αυτόματο κλείσιμο παρενθέσεων και εισαγωγικών"
|
||||
},
|
||||
"cm_autocompleteOnTyping": {
|
||||
"message": "Αυτόματη συμπλήρωση καθώς πληκτρολογείτε"
|
||||
"message": "Έλεγχος...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Χρήση καρτελών με έξυπνη εσοχή"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Συντομεύσεις πληκτρολογίου"
|
||||
"message": "Χρήση καρτελών με έξυπνη εσοχή",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Αναδίπλωση λέξεων"
|
||||
},
|
||||
"cm_matchHighlight": {
|
||||
"message": "Υπογράμμιση"
|
||||
},
|
||||
"cm_matchHighlightSelection": {
|
||||
"message": "Μόνο επιλογή"
|
||||
},
|
||||
"cm_resizeGripHint": {
|
||||
"message": "Διπλό κλικ για μεγιστοποίηση/επαναφορά ύψους"
|
||||
"message": "Αναδίπλωση λέξεων",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Χρήση έξυπνης εσοχής"
|
||||
"message": "Χρήση έξυπνης εσοχής",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Μέγεθος καρτέλας"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Θέμα"
|
||||
},
|
||||
"configOnChange": {
|
||||
"message": "στην αλλαγή"
|
||||
},
|
||||
"configOnChangeTooltip": {
|
||||
"message": "Αυτόματη αποθήκευση και εφαρμογή αλλαγών"
|
||||
},
|
||||
"configureStyle": {
|
||||
"message": "Ρυθμίσεις"
|
||||
},
|
||||
"configureStyleOnHomepage": {
|
||||
"message": "Ρυθμίσεις στην ιστοσελίδα"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Άκυρο"
|
||||
},
|
||||
"confirmClose": {
|
||||
"message": "Κλείσιμο"
|
||||
},
|
||||
"confirmDefault": {
|
||||
"message": "Χρήση προεπιλογής"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Διαγραφή"
|
||||
},
|
||||
"confirmDiscardChanges": {
|
||||
"message": "Απόρριψη αλλαγών;"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Όχι"
|
||||
},
|
||||
"confirmOK": {
|
||||
"message": "ΟΚ"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Αποθήκευση"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Ναι"
|
||||
},
|
||||
"connectingDropbox": {
|
||||
"message": "Σύνδεση με το Dropbox..."
|
||||
},
|
||||
"connectingDropboxNotAllowed": {
|
||||
"message": "Η σύνδεση με το Dropbox είναι διαθέσιμη μόνο σε εφαρμογές εγκατεστημένες απευθείας από το κατάστημα ιστού webstore"
|
||||
},
|
||||
"copied": {
|
||||
"message": "Αντιγράφηκε στο πρόχειρο"
|
||||
},
|
||||
"copy": {
|
||||
"message": "Αντιγραφή στο πρόχειρο"
|
||||
},
|
||||
"dateAbbrDay": {
|
||||
"message": "$value$μ",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAbbrHour": {
|
||||
"message": "$value$ω",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAbbrMonth": {
|
||||
"message": "$value$λ",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAbbrYear": {
|
||||
"message": "$value$χ",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateInstalled": {
|
||||
"message": "Ημερομηνία εγκατάστασης"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"message": "Ημερομηνία ενημέρωσης"
|
||||
"message": "Μέγεθος καρτέλας",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Παρουσιάστηκε σφάλμα χρησιμοποιώντας την κομψή βάση δεδομένων. Θα θέλατε να επισκεφθείτε μια ιστοσελίδα με πιθανές λύσεις;"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "προεπιλογή"
|
||||
"message": "Παρουσιάστηκε σφάλμα χρησιμοποιώντας την κομψή βάση δεδομένων. Θα θέλατε να επισκεφθείτε μια ιστοσελίδα με πιθανές λύσεις;",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;"
|
||||
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Διαγραφή"
|
||||
"message": "Διαγραφή",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"description": {
|
||||
"message": "Επαναπροσδιορίση του διαδίκτυου με το Stylus, έναν διαχειριστή στυλ. Το Stylus σας επιτρέπει να εγκαταστήσετε εύκολα themes και skins για πολλές δημοφιλείς ιστοσελίδες."
|
||||
"message": "Επαναπροσδιορίση του διαδίκτυου με το Stylus, έναν διαχειριστή στυλ. Το Stylus σας επιτρέπει να εγκαταστήσετε εύκολα themes και skins για πολλές δημοφιλείς ιστοσελίδες.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Απενεργοποιηση ολων των στυλ"
|
||||
"message": "Απενεργοποιηση ολων των στυλ",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Απενεργοποίηση"
|
||||
},
|
||||
"dragDropMessage": {
|
||||
"message": "Αποθέστε το αντίγραφο ασφαλείας σας οπουδήποτε σε αυτήν τη σελίδα για εισαγωγή."
|
||||
},
|
||||
"dragDropUsercssTabstrip": {
|
||||
"message": "Για να εγκαταστήσετε το αρχείο, αποθέστε το στη λωρίδα καρτελών (την περιοχή όπου εμφανίζονται οι τίτλοι καρτελών)."
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Διαγραφή"
|
||||
"message": "Απενεργοποίηση",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Μετάβαση στη γραμμή (ή line:col)"
|
||||
"message": "Μετάβαση στη γραμμή (ή line:col)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Επεξεργασία Στυλ"
|
||||
"message": "Επεξεργασία Στυλ",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Επεξεργασία"
|
||||
"message": "Επεξεργασία",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Επεξεργασία του στυλ $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -252,393 +134,92 @@
|
|||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Ενεργοποίηση"
|
||||
"message": "Ενεργοποίηση",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"excludeStyleByDomainLabel": {
|
||||
"message": "Εξαίρεση του τρέχοντος τομέα"
|
||||
},
|
||||
"excludeStyleByUrlLabel": {
|
||||
"message": "Εξαίρεση του τρέχοντος URL"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Εξαγωγή"
|
||||
},
|
||||
"exportSavedSuccess": {
|
||||
"message": "Το αρχείο αποθηκεύτηκε επιτυχώς."
|
||||
},
|
||||
"externalFeedback": {
|
||||
"message": "Σχόλια"
|
||||
},
|
||||
"externalHomepage": {
|
||||
"message": "Αρχική σελίδα"
|
||||
},
|
||||
"externalLink": {
|
||||
"message": "Εξωτερική σύνδεση"
|
||||
},
|
||||
"externalSupport": {
|
||||
"message": "Υποστήριξη"
|
||||
},
|
||||
"externalUsercssDocument": {
|
||||
"message": "Τεκμηρίωση για Usercss"
|
||||
},
|
||||
"filteredStyles": {
|
||||
"message": "Βλέπετε $numShown$ από 2$numTotal$ συνολικά",
|
||||
"placeholders": {
|
||||
"numShown": {
|
||||
"content": "$1"
|
||||
},
|
||||
"numTotal": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filteredStylesAllHidden": {
|
||||
"message": "Τα φίλτρα που εφαρμόζονται αυτήν τη στιγμή δεν ταιριάζουν με κανένα στυλ"
|
||||
},
|
||||
"findStyles": {
|
||||
"message": "Εύρεση στυλ"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Προσθήκη"
|
||||
},
|
||||
"genericClone": {
|
||||
"message": "Δημιουργία αντιγράφου"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Απενεργοποιημένο"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Ενεργοποιημένο"
|
||||
},
|
||||
"genericError": {
|
||||
"message": "Σφάλμα"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "Ιστορικό"
|
||||
},
|
||||
"genericNext": {
|
||||
"message": "Επόμενο"
|
||||
},
|
||||
"genericPrevious": {
|
||||
"message": "Προηγούμενο"
|
||||
},
|
||||
"genericResetLabel": {
|
||||
"message": "Επαναφορά"
|
||||
},
|
||||
"genericSavedMessage": {
|
||||
"message": "Αποθηκεύτηκε"
|
||||
},
|
||||
"genericTitle": {
|
||||
"message": "Τίτλος"
|
||||
},
|
||||
"genericUnknown": {
|
||||
"message": "Άγνωστο"
|
||||
},
|
||||
"gettingStyles": {
|
||||
"message": "Λήψη όλων των στυλ..."
|
||||
"findStylesForSite": {
|
||||
"message": "Αναζήτηση περισσότερων στυλ για αυτή την ιστοσελίδα",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Βοήθεια"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Πληκτρολογήστε μια εντολή"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Πληκτρολογήστε ένα hotkey"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Εισαγωγή"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Αντικατάσταση στυλ"
|
||||
},
|
||||
"importReportLegendAdded": {
|
||||
"message": "προστέθηκαν"
|
||||
},
|
||||
"importReportLegendUpdatedCode": {
|
||||
"message": "ενημερωμένος κώδικας"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Η εισαγωγή στυλ τελείωσε"
|
||||
},
|
||||
"importReportUnchanged": {
|
||||
"message": "Τίποτα δεν άλλαξε"
|
||||
},
|
||||
"importReportUndoneTitle": {
|
||||
"message": "Η εισαγωγή έχει αναιρεθεί"
|
||||
},
|
||||
"installButton": {
|
||||
"message": "Εγκατάσταση στυλ"
|
||||
},
|
||||
"installButtonInstalled": {
|
||||
"message": "Το στυλ έχει εγκατασταθεί."
|
||||
},
|
||||
"installButtonReinstall": {
|
||||
"message": "Επανεγκατάσταση στυλ"
|
||||
},
|
||||
"installButtonUpdate": {
|
||||
"message": "Ενημέρωση στυλ"
|
||||
"message": "Βοήθεια",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Εγκατάσταση ενημέρωσης"
|
||||
},
|
||||
"installUpdateFromLabel": {
|
||||
"message": "Έλεγχος για ενημερώσεις"
|
||||
},
|
||||
"license": {
|
||||
"message": "Άδεια χρήσης"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Βοήθεια"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Λήψη στυλ"
|
||||
},
|
||||
"linkTranslate": {
|
||||
"message": "Μετάφραση"
|
||||
},
|
||||
"linterConfigTooltip": {
|
||||
"message": "Πατήστε εδώ για να ρυθμίσετε το linter"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Ζητήματα"
|
||||
},
|
||||
"linterJSONError": {
|
||||
"message": "Μη έγκυρη μορφή JSON"
|
||||
},
|
||||
"linterResetMessage": {
|
||||
"message": "Για αναίρεση μιας κατά λάθος επαναφοράς, πατήστε Ctrl-Z (ή Cmd-Z) στο πλαίσιο κειμένου"
|
||||
"message": "Εγκατάσταση ενημέρωσης",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Φίλτρα"
|
||||
"message": "Φίλτρα",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Εγκατεστημένα Στυλ"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "Νέα διαχείριση διάταξης UI"
|
||||
},
|
||||
"manageOnlyDisabled": {
|
||||
"message": "Μόνο απενεργοποιημένα στυλ"
|
||||
"message": "Εγκατεστημένα Στυλ",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Μόνο ενεργοποιημένα στυλ"
|
||||
},
|
||||
"manageOnlyExternal": {
|
||||
"message": "Μόνο στυλ από άλλες ιστοσελίδες"
|
||||
},
|
||||
"manageOnlyLocal": {
|
||||
"message": "Μόνο στυλ δημιουργημένα τοπικά"
|
||||
"message": "Μόνο ενεργοποιημένα στυλ",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Κομψή"
|
||||
"message": "Κομψή",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Εμφάνιση ενεργους καταμέτρησης στυλ"
|
||||
},
|
||||
"noFileToImport": {
|
||||
"message": "Για να εισάγετε τα στυλ σας, πρέπει πρώτα να τα εξάγετε."
|
||||
"message": "Εμφάνιση ενεργους καταμέτρησης στυλ",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Δεν υπάρχουν εγκατεστημένα στυλ για αυτή την ιστοσελίδα."
|
||||
"message": "Δεν υπάρχουν εγκατεστημένα στυλ για αυτή την ιστοσελίδα.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Διαχείριση εγκατεστημένων στυλ"
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Επιλογές"
|
||||
},
|
||||
"openStylesManager": {
|
||||
"message": "Άνοιγμα διαχείρισης στυλ"
|
||||
},
|
||||
"optionsActions": {
|
||||
"message": "Ενέργειες"
|
||||
},
|
||||
"optionsAdvanced": {
|
||||
"message": "Για προχωρημένους"
|
||||
},
|
||||
"optionsAdvancedContextDelete": {
|
||||
"message": "Προσθήκη του 'Delete' στο μενού περιβάλλοντος του προγράμματος επεξεργασίας"
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Χρώμα φόντου όταν είναι απενεργοποιημένο"
|
||||
},
|
||||
"optionsBadgeNormal": {
|
||||
"message": "Χρώμα υποβάθρου"
|
||||
},
|
||||
"optionsCheck": {
|
||||
"message": "Ενημέρωση στυλ"
|
||||
},
|
||||
"optionsCheckUpdate": {
|
||||
"message": "Έλεγχος και εγκατάσταση διαθέσιμων ενημερώσεων"
|
||||
},
|
||||
"optionsCustomizeBadge": {
|
||||
"message": "Σήμα στο εικονίδιο της γραμμής εργαλείων"
|
||||
},
|
||||
"optionsCustomizePopup": {
|
||||
"message": "Αναδυόμενο παράθυρο"
|
||||
},
|
||||
"optionsCustomizeUpdate": {
|
||||
"message": "Ενημερώσεις"
|
||||
"message": "Διαχείριση εγκατεστημένων στυλ",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Επιλογές"
|
||||
},
|
||||
"optionsIconDark": {
|
||||
"message": "Σκούρο θέμα φυλλομετρητή"
|
||||
},
|
||||
"optionsOpen": {
|
||||
"message": "Άνοιγμα"
|
||||
},
|
||||
"optionsOpenManager": {
|
||||
"message": "Διαχείριση στυλ"
|
||||
},
|
||||
"optionsPopupWidth": {
|
||||
"message": "Πλάτος αναδυόμενου παραθύρου (σε pixels)"
|
||||
},
|
||||
"optionsReset": {
|
||||
"message": "Επαναφορά ρυθμίσεων στις προεπιλεγμένες"
|
||||
},
|
||||
"optionsResetButton": {
|
||||
"message": "Επαναφορά επιλογών"
|
||||
},
|
||||
"optionsSubheading": {
|
||||
"message": "Περισσότερες επιλογές"
|
||||
},
|
||||
"optionsSyncConnect": {
|
||||
"message": "Σύνδεση"
|
||||
},
|
||||
"optionsSyncDisconnect": {
|
||||
"message": "Αποσύνδεση"
|
||||
},
|
||||
"optionsSyncStatusConnected": {
|
||||
"message": "Συνδεδεμένο"
|
||||
},
|
||||
"optionsSyncStatusConnecting": {
|
||||
"message": "Σύνδεση..."
|
||||
},
|
||||
"optionsSyncStatusDisconnected": {
|
||||
"message": "Αποσυνδέθηκε"
|
||||
},
|
||||
"optionsSyncStatusDisconnecting": {
|
||||
"message": "Αποσύνδεση..."
|
||||
},
|
||||
"optionsSyncStatusSyncing": {
|
||||
"message": "Συγχρονισμός ..."
|
||||
},
|
||||
"optionsSyncSyncNow": {
|
||||
"message": "Συγχρονισμός τώρα"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "διεύθυνση URL"
|
||||
},
|
||||
"optionsUpdateInterval": {
|
||||
"message": "Διάστημα αυτόματης ενημέρωσης των στυλ σε ώρες (0 για απενεργοποίηση)"
|
||||
},
|
||||
"paginationNext": {
|
||||
"message": "Επόμενη σελίδα"
|
||||
},
|
||||
"paginationPrevious": {
|
||||
"message": "Προηγούμενη σελίδα"
|
||||
},
|
||||
"popupBordersTooltip": {
|
||||
"message": "Χρήσιμο για σκούρα θέματα στο καινούριο Chrome, καθώς δε βάφει πλέον τα ακριανά περιθώρια."
|
||||
},
|
||||
"popupOpenEditInPopup": {
|
||||
"message": "Χρήση ενός απλού παραθύρου (χωρίς omnibox)"
|
||||
},
|
||||
"popupOpenEditInWindow": {
|
||||
"message": "Άνοιγμα επεξαργαστή σε νέο παράθυρο"
|
||||
"message": "Επιλογές",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "Στυλ λίστας πριν των εντολών στο μενού του κουμπιού γραμμής εργαλείων"
|
||||
"message": "Στυλ λίστας πριν των εντολών στο μενού του κουμπιού γραμμής εργαλείων",
|
||||
"description": "Label for the checkbox controlling section order in the popup."
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Εμφάνιση αριθμού των στυλ που δραστηριοποιούνται για την τρέχουσα τοποθεσία στην μπάρα εργαλείων"
|
||||
},
|
||||
"readingStyles": {
|
||||
"message": "Ανάγνωση στυλ..."
|
||||
},
|
||||
"replace": {
|
||||
"message": "Αντικατάσταση"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Αντικατάσταση όλων"
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Αντικατάσταση με"
|
||||
},
|
||||
"retrieveBckp": {
|
||||
"message": "Εισαγωγή στυλ"
|
||||
},
|
||||
"retrieveDropboxSync": {
|
||||
"message": "Εισαγωγή από το Dropbox"
|
||||
},
|
||||
"search": {
|
||||
"message": "Αναζήτηση"
|
||||
},
|
||||
"searchGlobalStyles": {
|
||||
"message": "Επίσης, αναζητήστε καθολικά στυλ"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Χρησιμοποιήστε τη σύνταξη /re/ για αναζήτηση με regexp."
|
||||
},
|
||||
"searchResultInstallCount": {
|
||||
"message": "Συνολικός αριθμός εγκαταστάσεων"
|
||||
},
|
||||
"searchResultUpdated": {
|
||||
"message": "Ενημερωμένο"
|
||||
},
|
||||
"searchResultWeeklyCount": {
|
||||
"message": "Εβδομαδιαίος αριθμός εγκαταστάσεων"
|
||||
},
|
||||
"searchStylesName": {
|
||||
"message": "Όνομα"
|
||||
"message": "Εμφάνιση αριθμού των στυλ που δραστηριοποιούνται για την τρέχουσα τοποθεσία στην μπάρα εργαλείων",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Προσθήκη ένος άλλου τμήματος"
|
||||
"message": "Προσθήκη ένος άλλου τμήματος",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Κώδικας"
|
||||
"message": "Κώδικας",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Αφαίρεση ενότητας"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Ενότητες"
|
||||
},
|
||||
"shortcuts": {
|
||||
"message": "Συντομεύσεις"
|
||||
},
|
||||
"sortDateNewestFirst": {
|
||||
"message": "πιο πρόσφατα πρώτα"
|
||||
},
|
||||
"sortDateOldestFirst": {
|
||||
"message": "πιο παλιά πρώτα"
|
||||
"message": "Αφαίρεση ενότητας",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Το Regexp δεν είναι έγκυρο."
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Ωραιοποίηση"
|
||||
},
|
||||
"styleBeautifyIndentConditional": {
|
||||
"message": "Διόρθωση εσοχής για @media και @supports"
|
||||
},
|
||||
"styleBeautifyPreserveNewlines": {
|
||||
"message": "Διατήρηση νέων γραμμών (newlines)"
|
||||
"message": "Το Regexp δεν είναι έγκυρο.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Πίσω στη διαχείριση"
|
||||
"message": "Πίσω στη διαχείριση",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Έχετε κάνει αλλαγές σε αυτό το ύφος χωρίς αποθήκευση."
|
||||
"message": "Έχετε κάνει αλλαγές σε αυτό το ύφος χωρίς αποθήκευση.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Ενεργοποιημένη"
|
||||
"message": "Ενεργοποιημένη",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Εγκατάσταση του '$stylename$' στο Stylus;",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -646,19 +227,20 @@
|
|||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Εισάγετε ένα όνομα"
|
||||
},
|
||||
"styleRegexpTestNone": {
|
||||
"message": "Δε βρέθηκαν καρτέλες που αντιστοιχούν."
|
||||
"message": "Εισάγετε ένα όνομα",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Αποθήκευση"
|
||||
"message": "Αποθήκευση",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Η μορφή του Mozilla κώδικα μπορεί να χρησιμοποιηθεί με το Stylish για το Firefox και μπορεί να υποβληθεί στο userstyles.org."
|
||||
"message": "Η μορφή του Mozilla κώδικα μπορεί να χρησιμοποιηθεί με το Stylish για το Firefox και μπορεί να υποβληθεί στο userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"styleUpdate": {
|
||||
"message": "Είστε σίγουροι ότι θέλετε να ενημερώσετε το '$stylename$';",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -666,40 +248,16 @@
|
|||
}
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή."
|
||||
},
|
||||
"stylusUnavailableForURLdetails": {
|
||||
"message": "Ως μέτρο ασφαλείας, ο φυλλομετρητής απαγορεύει στα πρόσθετα να επέμβουν στις built-in σελίδες του (όπως π.χ. chrome://version, η σελίδα νέας καρτέλας από το Chrome 61 και μετά, about:addons, κλπ.), καθώς και τις σελίδες άλλωων προσθέτων. Επιπλέον, κάθε φυλλομετρητής περιορίζει την πρόσβαση στο κατάστημα προσθέτων (όπως το Chrome Web Store ή το AMO)."
|
||||
},
|
||||
"syncDropboxStyles": {
|
||||
"message": "Εξαγωγή από το Dropbox"
|
||||
},
|
||||
"syncError": {
|
||||
"message": "Ο συγχρονισμός απέτυχε"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Αλλαγή στυλ"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Αναίρεση"
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Αναίρεση όλων των ενεργειών"
|
||||
},
|
||||
"unreachableFileHint": {
|
||||
"message": "Το Stylus έχει πρόσβαση στις file:// διευθύνσεις URL μόνο αν έχετε επιλέξει το αντίστοιχο πλαίσιο ελέγχου για το πρόσθετο Stylus στη σελίδα chrome://extensions."
|
||||
},
|
||||
"unzipStyles": {
|
||||
"message": "Αποσυμπίεση στυλ..."
|
||||
"message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Όλα τα στυλ είναι ενημερωμένα."
|
||||
},
|
||||
"updateAllCheckSucceededSomeEdited": {
|
||||
"message": "Δεν έχει γίνει έλεγχος ενημερώσεων για κάποια στυλ, για να αποφευχθεί η πιθανότητα απώλειας τοπικών επεξεργασιών. Οι ενημερώσεις μπορούν να εξαναγκαστούν ελέγχοντας το κάθε στυλ ξεχωριστά ή ελέγχοντας πάλι όλα τα στυλ (τοπικές επεξεργασίες θα αντικατασταθούν)"
|
||||
"message": "Όλα τα στυλ είναι ενημερωμένα.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Αποτυχία ενημέρωσης: ο διακομιστής ανταποκρίθηκε με κωδικό $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
|
@ -707,39 +265,23 @@
|
|||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Αποτυχία ενημέρωσης: απρόσιτος διακομιστής."
|
||||
},
|
||||
"updateCheckSkippedLocallyEdited": {
|
||||
"message": "Το στυλ επεξεργάστηκε τοπικά στον υπολογιστή σας."
|
||||
},
|
||||
"updateCheckSkippedMaybeLocallyEdited": {
|
||||
"message": "Το στυλ αυτό μπορεί να έχει επεξεργαστεί τοπικά στον υπολογιστή σας."
|
||||
"message": "Αποτυχία ενημέρωσης: απρόσιτος διακομιστής.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Το στυλ είναι ενημερωμένο."
|
||||
"message": "Το στυλ είναι ενημερωμένο.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Η ενημέρωση ολοκληρώθηκε."
|
||||
},
|
||||
"updatesCurrentlyInstalled": {
|
||||
"message": "Ενημερώσεις που εγκαταστάθηκαν"
|
||||
},
|
||||
"uploadingFile": {
|
||||
"message": "Μεταφόρτωση αρχείου..."
|
||||
},
|
||||
"usercssEditorNamePlaceholder": {
|
||||
"message": "Καθορίστε το @name στον κώδικα"
|
||||
},
|
||||
"versionInvalidOlder": {
|
||||
"message": "Η έκδοση αυτή είναι παλαιότερη από αυτήν που είναι ήδη εγκατεστημένη."
|
||||
"message": "Η ενημέρωση ολοκληρώθηκε.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Γράψτε νέο στυλ για:"
|
||||
"message": "Γράψτε νέο στυλ για:",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "αυτή την διεύθυνση URL"
|
||||
},
|
||||
"zipStyles": {
|
||||
"message": "Συμπίεση στυλ..."
|
||||
"message": "αυτή την διεύθυνση URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,51 +1,71 @@
|
|||
{
|
||||
"appliesRemoveError": {
|
||||
"message": "Cannot remove last 'applies to' entry"
|
||||
"message": "Cannot remove last 'applies to' entry",
|
||||
"description": "Error displayed when the last 'applies' is going to be removed"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Check again—I didn't edit any styles!"
|
||||
"message": "Check again—I didn't edit any styles!",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"cm_autoCloseBrackets": {
|
||||
"message": "Auto-close brackets and quotes"
|
||||
"message": "Auto-close brackets and quotes",
|
||||
"description": "Label for the checkbox in the style editor."
|
||||
},
|
||||
"cm_colorpicker": {
|
||||
"message": "Colour pickers for CSS colours"
|
||||
"message": "Colour pickers for CSS colours",
|
||||
"description": "Label for the checkbox controlling colorpicker option for the style editor."
|
||||
},
|
||||
"cm_resizeGripHint": {
|
||||
"message": "Double-click to maximise/restore the height"
|
||||
"message": "Double-click to maximise/restore the height",
|
||||
"description": "Tooltip for the resize grip in style editor"
|
||||
},
|
||||
"colorpickerTooltip": {
|
||||
"message": "Open colour picker"
|
||||
"message": "Open colour picker",
|
||||
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
|
||||
},
|
||||
"description": {
|
||||
"message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites."
|
||||
"message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Go to line (or line:col)"
|
||||
"message": "Go to line (or line:col)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Edit style"
|
||||
"message": "Edit style",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"installUpdateUnavailable": {
|
||||
"message": "To enable checking for updates, drop the file on the tab strip or specify @updateURL in the style metadata.",
|
||||
"description": ""
|
||||
},
|
||||
"license": {
|
||||
"message": "Licence"
|
||||
"message": "Licence",
|
||||
"description": "Label for the license"
|
||||
},
|
||||
"manageFaviconsGray": {
|
||||
"message": "Greyed out"
|
||||
"message": "Greyed out",
|
||||
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Background colour when disabled"
|
||||
"message": "Background colour when disabled",
|
||||
"description": ""
|
||||
},
|
||||
"optionsBadgeNormal": {
|
||||
"message": "Background colour"
|
||||
"message": "Background colour",
|
||||
"description": ""
|
||||
},
|
||||
"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."
|
||||
"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.",
|
||||
"description": ""
|
||||
},
|
||||
"optionsUpdateInterval": {
|
||||
"message": "Userstyle auto-update interval in hours (specify 0 to disable)"
|
||||
"message": "Userstyle auto-update interval in hours (specify 0 to disable)",
|
||||
"description": ""
|
||||
},
|
||||
"styleInstallFailed": {
|
||||
"message": "Failed to install userstyle\n$error$",
|
||||
"description": "Warning when installation failed",
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"content": "$1"
|
||||
|
@ -53,12 +73,15 @@
|
|||
}
|
||||
},
|
||||
"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)."
|
||||
"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).",
|
||||
"description": ""
|
||||
},
|
||||
"styleUpdateDiscardChanges": {
|
||||
"message": "The style has been changed outside the editor. Would you like to reload the style?"
|
||||
"message": "The style has been changed outside the editor. Would you like to reload the style?",
|
||||
"description": "Confirmation to update the style in the editor"
|
||||
},
|
||||
"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:"
|
||||
"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:",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
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,15 +1,19 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Uusi Tyyli"
|
||||
"message": "Uusi Tyyli",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Lisää Tyyli"
|
||||
"message": "Lisää Tyyli",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Lisää"
|
||||
"message": "Lisää",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Kooskee: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -17,70 +21,80 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "ja lisää"
|
||||
"message": "ja lisää",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL ositteita domainilla"
|
||||
"message": "URL ositteita domainilla",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee."
|
||||
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Koskee"
|
||||
"message": "Koskee",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL ositteet jotka vastaavat regexpiä"
|
||||
"message": "URL ositteet jotka vastaavat regexpiä",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Poista"
|
||||
"message": "Poista",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Tarkenna"
|
||||
"message": "Tarkenna",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Kaikki"
|
||||
"message": "Kaikki",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL osoitteet jotka alkavat"
|
||||
"message": "URL osoitteet jotka alkavat",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Tarkista kaikki tyylit päivityksien varalta"
|
||||
"message": "Tarkista kaikki tyylit päivityksien varalta",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Hae päivityksiä"
|
||||
"message": "Hae päivityksiä",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Tarkistetaan..."
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Poista"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Tallenna"
|
||||
"message": "Tarkistetaan...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Oletko varma että haluat poistaa tämän tyylin?"
|
||||
"message": "Oletko varma että haluat poistaa tämän tyylin?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Poista"
|
||||
"message": "Poista",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"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."
|
||||
"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"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Poista Käytöstä"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Poista"
|
||||
"message": "Poista Käytöstä",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Muokkaa Tyyliä"
|
||||
"message": "Muokkaa Tyyliä",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Muokkaa"
|
||||
"message": "Muokkaa",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Muokkaa Tyyliä $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -88,64 +102,76 @@
|
|||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Aktivoi"
|
||||
"message": "Aktivoi",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Lisää"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Aktivoitu"
|
||||
"findStylesForSite": {
|
||||
"message": "Hae lisää tyylejä tälle sivustolle",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Apu"
|
||||
"message": "Apu",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Asenna päivitys"
|
||||
"message": "Asenna päivitys",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Asennetut Tyylit"
|
||||
"message": "Asennetut Tyylit",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "Tyylikäs"
|
||||
"message": "Tyylikäs",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Ei asennettuja tyylejä tällä sivustolla."
|
||||
"message": "Ei asennettuja tyylejä tällä sivustolla.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Hallitse asennettuja tyylejä"
|
||||
"message": "Hallitse asennettuja tyylejä",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"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 popup."
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Show number of styles active for the current site on the toolbar button"
|
||||
"message": "Show number of styles active for the current site on the toolbar button",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Lisää uusi osio"
|
||||
"message": "Lisää uusi osio",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Koodi"
|
||||
"message": "Koodi",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Poista osio"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Osiot"
|
||||
"message": "Poista osio",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp ei kelpaa."
|
||||
"message": "Regexp ei kelpaa.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Takaisin hallintapaneeliin"
|
||||
"message": "Takaisin hallintapaneeliin",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta."
|
||||
"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"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Aktivoitu"
|
||||
"message": "Aktivoitu",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -153,19 +179,24 @@
|
|||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Syötä nimi"
|
||||
"message": "Syötä nimi",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Tallenna"
|
||||
"message": "Tallenna",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin."
|
||||
"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"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "All styles are up to date."
|
||||
"message": "All styles are up to date.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"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"
|
||||
|
@ -173,12 +204,15 @@
|
|||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen."
|
||||
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Tyyli on ajan tasalla."
|
||||
"message": "Tyyli on ajan tasalla.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Päivitys suoritettu."
|
||||
"message": "Päivitys suoritettu.",
|
||||
"description": "Text that displays when an update completed"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +1,19 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Nije styl skriuwe"
|
||||
"message": "Nije styl skriuwe",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Styl tafoegje"
|
||||
"message": "Styl tafoegje",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Tafoegje"
|
||||
"message": "Tafoegje",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Fan tapassing op: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -17,81 +21,107 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "en mear"
|
||||
"message": "en mear",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL’s op it domein"
|
||||
"message": "URL’s op it domein",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Brûk de ‘Fan tapassing op’-funksjes om de URL’s foar de koade yn dizze seksje te beheinen."
|
||||
"message": "Brûk de ‘Fan tapassing op’-funksjes om de URL’s foar de koade yn dizze seksje te beheinen.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Fan tapassing op"
|
||||
"message": "Fan tapassing op",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL’s oerienkommend mei de regexp"
|
||||
"message": "URL’s oerienkommend mei de regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Fuortsmite"
|
||||
"message": "Fuortsmite",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Spesifisearje"
|
||||
"message": "Spesifisearje",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Alles"
|
||||
"message": "Alles",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL’s begjinnend mei"
|
||||
"message": "URL’s begjinnend mei",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Alle fernijingen tapasse"
|
||||
"message": "Alle fernijingen tapasse",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Alle stilen kontrolearje op fernijingen"
|
||||
"message": "Alle stilen kontrolearje op fernijingen",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Kontrolearje op fernijing"
|
||||
"message": "Kontrolearje op fernijing",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Kontrolearje..."
|
||||
"message": "Kontrolearje...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Ljepblêden mei tûke ynspringing brûke"
|
||||
"message": "Ljepblêden mei tûke ynspringing brûke",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Toetseboerdyndieling"
|
||||
"message": "Toetseboerdyndieling",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Teksttebekrin"
|
||||
"message": "Teksttebekrin",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Tûke ynspringing brûke"
|
||||
"message": "Tûke ynspringing brûke",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Ljepblêdgrutte"
|
||||
"message": "Ljepblêdgrutte",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Tema"
|
||||
"message": "Tema",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Nee"
|
||||
"message": "Nee",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Stoppe"
|
||||
"message": "Stoppe",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Ja"
|
||||
"message": "Ja",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?"
|
||||
"message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "standert"
|
||||
"message": "standert",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Binne jo wis dat jo dizze styl fuortsmite wolle?"
|
||||
"message": "Binne jo wis dat jo dizze styl fuortsmite wolle?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Fuortsmite"
|
||||
"message": "Fuortsmite",
|
||||
"description": "Label for the button to delete a style"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
{
|
||||
"addStyleTitle": {
|
||||
"message": "Engadir Estilo"
|
||||
"message": "Engadir Estilo",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Opacidade"
|
||||
"message": "Opacidade",
|
||||
"description": "Label of color's opacity"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Engadir"
|
||||
"message": "Engadir",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Aplica a: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -17,36 +21,47 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "e mais"
|
||||
"message": "e mais",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URLs no dominio"
|
||||
"message": "URLs no dominio",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Aplica para"
|
||||
"message": "Aplica para",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesLineWidgetWarning": {
|
||||
"message": "Non funciona con CSS minificado"
|
||||
"message": "Non funciona con CSS minificado",
|
||||
"description": "A warning that applies-to information won't show properly with minified CSS"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URLs que concorden co regexp"
|
||||
"message": "URLs que concorden co regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Suprimir"
|
||||
"message": "Suprimir",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Especificar"
|
||||
"message": "Especificar",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Todo"
|
||||
"message": "Todo",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URLs que comecen por"
|
||||
"message": "URLs que comecen por",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Aplicar tódalas actualizacións"
|
||||
"message": "Aplicar tódalas actualizacións",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"author": {
|
||||
"message": "Autor"
|
||||
"message": "Autor",
|
||||
"description": "Label for the style author"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
1
_locales/hr/messages.json
Normal file
1
_locales/hr/messages.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
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
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +1,19 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Упиши нови стил"
|
||||
"message": "Упиши нови стил",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Додај стил"
|
||||
"message": "Додај стил",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Додај"
|
||||
"message": "Додај",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Примењује се на: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -17,115 +21,140 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "и још"
|
||||
"message": "и још",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "УРЛ адресе на домену"
|
||||
"message": "УРЛ адресе на домену",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује."
|
||||
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Примењује се на"
|
||||
"message": "Примењује се на",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "УРЛ адресе које одговарају регуларном изразу"
|
||||
"message": "УРЛ адресе које одговарају регуларном изразу",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Уклони"
|
||||
"message": "Уклони",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Детаљније"
|
||||
"message": "Детаљније",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Све"
|
||||
"message": "Све",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlOption": {
|
||||
"message": "УРЛ"
|
||||
"message": "УРЛ",
|
||||
"description": "Option to make the style apply to the entered string as a URL"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "УРЛ адресе које почињу са"
|
||||
"message": "УРЛ адресе које почињу са",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Примени сва ажурирања"
|
||||
"message": "Примени сва ажурирања",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Проверите ажурирања за све стилове"
|
||||
"message": "Проверите ажурирања за све стилове",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Проверите ажурирање"
|
||||
"message": "Проверите ажурирање",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Проверавање..."
|
||||
"message": "Проверавање...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Користи картице са паметним увлачењем редова"
|
||||
"message": "Користи картице са паметним увлачењем редова",
|
||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Мапа тастера"
|
||||
"message": "Мапа тастера",
|
||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Преламање текста"
|
||||
"message": "Преламање текста",
|
||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Користи паметно увлачење редова"
|
||||
"message": "Користи паметно увлачење редова",
|
||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Величина картице"
|
||||
"message": "Величина картице",
|
||||
"description": "Label for the text box controlling tab size option for the style editor."
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Тема"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Избриши"
|
||||
"message": "Тема",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Не"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Сачувај"
|
||||
"message": "Не",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Заустави"
|
||||
"message": "Заустави",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Да"
|
||||
"message": "Да",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?"
|
||||
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?",
|
||||
"description": "Prompt when a DB error is encountered"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "подразумевано"
|
||||
"message": "подразумевано",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Да ли сте сигурни да желите да избришете овај стил?"
|
||||
"message": "Да ли сте сигурни да желите да избришете овај стил?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Избриши"
|
||||
"message": "Избриши",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"description": {
|
||||
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове."
|
||||
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Искључи све стилове"
|
||||
"message": "Искључи све стилове",
|
||||
"description": "Label for the checkbox that turns all enabled styles off."
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Онемогући"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Избриши"
|
||||
"message": "Онемогући",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Иди на ред (или line:col)"
|
||||
"message": "Иди на ред (или line:col)",
|
||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Уреди стил"
|
||||
"message": "Уреди стил",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Уреди"
|
||||
"message": "Уреди",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Уреди стил $stylename$",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -133,55 +162,68 @@
|
|||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Омогући"
|
||||
"message": "Омогући",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Извези"
|
||||
"message": "Извези",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Додај"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Омогућено"
|
||||
"findStylesForSite": {
|
||||
"message": "Пронађи још стилова за овај сајт",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Помоћ"
|
||||
"message": "Помоћ",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Укуцај име команде"
|
||||
"message": "Укуцај име команде",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Притисни пречицу"
|
||||
"message": "Притисни пречицу",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Додај стилу"
|
||||
"message": "Додај стилу",
|
||||
"description": "Label for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "Додај увезени стил тренутном стилу"
|
||||
"message": "Додај увезени стил тренутном стилу",
|
||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Увези"
|
||||
"message": "Увези",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Упиши преко стила"
|
||||
"message": "Упиши преко стила",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил"
|
||||
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил",
|
||||
"description": "Label for the button to import and overwrite current style"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Инсталирај ажурирање"
|
||||
"message": "Инсталирај ажурирање",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Помоћ"
|
||||
"message": "Помоћ",
|
||||
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Преузмите стилове"
|
||||
"message": "Преузмите стилове",
|
||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Проблеми"
|
||||
"message": "Проблеми",
|
||||
"description": "Label for the CSS linter issues block on the style edit page"
|
||||
},
|
||||
"linterIssuesHelp": {
|
||||
"message": "Проблем пронађен од стране $link$:",
|
||||
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"content": "$1"
|
||||
|
@ -189,85 +231,104 @@
|
|||
}
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Филтери"
|
||||
"message": "Филтери",
|
||||
"description": "Label for filters container"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Инсталирани стилови"
|
||||
"message": "Инсталирани стилови",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Само омогућени стилови"
|
||||
"message": "Само омогућени стилови",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Прикажи број активних стилова"
|
||||
"message": "Прикажи број активних стилова",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Нема инсталираних стилова за овај сајт."
|
||||
"message": "Нема инсталираних стилова за овај сајт.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Управљај инсталираним стиловима"
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Опције"
|
||||
"message": "Управљај инсталираним стиловима",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Опције"
|
||||
},
|
||||
"optionsSyncUrl": {
|
||||
"message": "УРЛ"
|
||||
"message": "Опције",
|
||||
"description": "Heading for options section on manage page."
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "Излистај стилове пре команди у менију дугмета на алатној траци"
|
||||
"message": "Излистај стилове пре команди у менију дугмета на алатној траци",
|
||||
"description": "Label for the checkbox controlling section order in the popup."
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци"
|
||||
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"replace": {
|
||||
"message": "Замени"
|
||||
"message": "Замени",
|
||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Замени све"
|
||||
"message": "Замени све",
|
||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Замени са"
|
||||
"message": "Замени са",
|
||||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||
},
|
||||
"search": {
|
||||
"message": "Претражи"
|
||||
"message": "Претражи",
|
||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Користи /re/ синтаксу за претрагу регуларним изразом"
|
||||
"message": "Користи /re/ синтаксу за претрагу регуларним изразом",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
},
|
||||
"searchStyles": {
|
||||
"message": "Претражи садржај",
|
||||
"description": "Label for the search filter textbox on the Manage styles page"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Додај нови одељак"
|
||||
"message": "Додај нови одељак",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Код"
|
||||
"message": "Код",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Уклони одељак"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Одељци"
|
||||
"message": "Уклони одељак",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Регуларни израз је неисправан."
|
||||
"message": "Регуларни израз је неисправан.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Улепшај"
|
||||
"message": "Улепшај",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Назад на управљање"
|
||||
"message": "Назад на управљање",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Направили сте измене овог стила које нисте сачували."
|
||||
"message": "Направили сте измене овог стила које нисте сачували.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Омогућено"
|
||||
"message": "Омогућено",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Налепи код у Mozilla формату"
|
||||
"message": "Налепи код у Mozilla формату",
|
||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "Инсталирати '$stylename$' у Stylus?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -275,22 +336,28 @@
|
|||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Унесите назив"
|
||||
"message": "Унесите назив",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla формат"
|
||||
"message": "Mozilla формат",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Сачувај"
|
||||
"message": "Сачувај",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org."
|
||||
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"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$'?",
|
||||
"description": "Confirmation when updating a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -298,19 +365,24 @@
|
|||
}
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus не ради на страницама као што је ова."
|
||||
"message": "Stylus не ради на страницама као што је ова.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Опозови"
|
||||
"message": "Опозови",
|
||||
"description": "Button label"
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Опозови (свеобухватно)"
|
||||
"message": "Опозови (свеобухватно)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Сви стилови су ажурирани."
|
||||
"message": "Сви стилови су ажурирани.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
|
@ -318,18 +390,23 @@
|
|||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Ажурирање није успело: сервер није доступан."
|
||||
"message": "Ажурирање није успело: сервер није доступан.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Стил је ажуриран."
|
||||
"message": "Стил је ажуриран.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Ажурирање је комплетирано."
|
||||
"message": "Ажурирање је комплетирано.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Упиши стил за:"
|
||||
"message": "Упиши стил за:",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "ову УРЛ адресу"
|
||||
"message": "ову УРЛ адресу",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "క్రొత్త స్టైల్ వ్రాయండి"
|
||||
"message": "క్రొత్త స్టైల్ వ్రాయండి",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "చేర్చు"
|
||||
"message": "చేర్చు",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "వేటికి వర్తిస్తుంది; $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -14,54 +17,51 @@
|
|||
}
|
||||
},
|
||||
"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": "భద్రపరచు"
|
||||
"message": "అన్నిటికీ",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?"
|
||||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "తొలగించు"
|
||||
"message": "తొలగించు",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "అచేతనించు"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "తొలగించు"
|
||||
"message": "అచేతనించు",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "మార్చు"
|
||||
"message": "మార్చు",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "చేతనించు"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "చేర్చు"
|
||||
"message": "చేతనించు",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "సహాయం"
|
||||
"message": "సహాయం",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "స్థాపిత శైలులు"
|
||||
"message": "స్థాపిత శైలులు",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"manageTitle": {
|
||||
"message": "స్టైలిష్"
|
||||
},
|
||||
"sections": {
|
||||
"message": "విభాగాలు"
|
||||
"message": "స్టైలిష్",
|
||||
"description": "Title for the manage page"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "భద్రపరచు"
|
||||
"message": "భద్రపరచు",
|
||||
"description": "Label for save button for style editing"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"addStyleLabel": {
|
||||
"message": "Yeni stil oluşturun"
|
||||
"message": "Yeni stil oluşturun",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Stil Ekleyin"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Opaklık"
|
||||
"message": "Stil Ekleyin",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Ekleyin"
|
||||
"message": "Ekleyin",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Şuraya uygulanır: $applies$",
|
||||
"description": "Text on the manage screen to describe what the style applies to",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
|
@ -20,217 +21,80 @@
|
|||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "ve diğerleri"
|
||||
"message": "ve diğerleri",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "Alan adındaki URLler"
|
||||
"message": "Alan adındaki URLler",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Bu bölümdeki kodun hangi URLlere uygulanacağını sınırlamak için 'Şuraya uygulanır' denetimlerini kullanın."
|
||||
"message": "Bu bölümdeki kodun hangi URLlere uygulanacağını sınırlamak için 'Şuraya uygulanır' denetimlerini kullanın.",
|
||||
"description": "Help text for 'applies to' section"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Şuraya uygulanır"
|
||||
},
|
||||
"appliesLineWidgetWarning": {
|
||||
"message": "Küçültülmüş CSS ile çalışmaz"
|
||||
"message": "Şuraya uygulanır",
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "regexp ile eşleşen URL'ler"
|
||||
"message": "regexp ile eşleşen URL'ler",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Kaldır"
|
||||
"message": "Kaldır",
|
||||
"description": "Label for the button to remove an 'applies' entry"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Belirt"
|
||||
"message": "Belirt",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Her şey"
|
||||
"message": "Her şey",
|
||||
"description": "Text displayed for styles that apply to all sites"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "Şununla başlayan URL'ler:"
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Tüm güncellemeleri uygula"
|
||||
},
|
||||
"author": {
|
||||
"message": "Yazar"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Yedek"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Dışa aktar"
|
||||
"message": "Şununla başlayan URL'ler:",
|
||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Tüm stiller için güncellemeleri denetle"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Tekrar kontrol et, herhangi bir stil düzenlemedim!"
|
||||
"message": "Tüm stiller için güncellemeleri denetle",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Güncellemeleri denetle"
|
||||
"message": "Güncellemeleri denetle",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Kontrol ediliyor..."
|
||||
},
|
||||
"clickToUninstall": {
|
||||
"message": "Kaldırmak için tıklayın"
|
||||
},
|
||||
"cm_autoCloseBrackets": {
|
||||
"message": "Parantezleri ve tırnak işaretlerini otomatik olarak kapat"
|
||||
},
|
||||
"cm_autoCloseBracketsTooltip": {
|
||||
"message": "()[]{}''\"\" Açılışlarından birini yazarken otomatik olarak bir kapanış çifti ekleyin"
|
||||
},
|
||||
"cm_autocompleteOnTyping": {
|
||||
"message": "Yazarken tamamla"
|
||||
},
|
||||
"cm_colorpicker": {
|
||||
"message": "CSS renk seçicileri"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Akıllı girintili tab kullan"
|
||||
},
|
||||
"cm_keyMap": {
|
||||
"message": "Tuşeşlem"
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Kelime kaydırma"
|
||||
},
|
||||
"cm_matchHighlight": {
|
||||
"message": "Vurgulama"
|
||||
},
|
||||
"cm_matchHighlightSelection": {
|
||||
"message": "Yalnızca seçim"
|
||||
},
|
||||
"cm_matchHighlightToken": {
|
||||
"message": "İmleç altındaki token"
|
||||
},
|
||||
"cm_resizeGripHint": {
|
||||
"message": "Yüksekliği en üst düzeye çıkarmak/geri yüklemek için çift tıklayın"
|
||||
},
|
||||
"cm_selectByTokens": {
|
||||
"message": "Çift tıklamak tokenleri seçer"
|
||||
},
|
||||
"cm_selectByTokensTooltip": {
|
||||
"message": "Token örnekleri: .foo-bar-2 #aabbcc 0.32 !important\nDevre dışı bırakıldığında: noktalama işareti ile ayrılmış kelimeler seçilir."
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Akıllı girinti kullanma"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Tab büyüklüğü"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Tema"
|
||||
},
|
||||
"colorpickerSwitchFormatTooltip": {
|
||||
"message": "Format değişimleri: HEX -> RGB -> HSL.\nYönü ters çevirmek için Shift tuşunu basılı tutup tıklatın.\nAyrıca PgUp (PageUp), PgDn (PageDown) tuşları ile."
|
||||
},
|
||||
"colorpickerTooltip": {
|
||||
"message": "Renk seçiciyi aç"
|
||||
},
|
||||
"configOnChange": {
|
||||
"message": "değişiklikte"
|
||||
},
|
||||
"configOnChangeTooltip": {
|
||||
"message": "Otomatik kaydetme ve değişiklikleri otomatik olarak uygulama"
|
||||
},
|
||||
"configureStyle": {
|
||||
"message": "Yapılandır"
|
||||
},
|
||||
"configureStyleOnHomepage": {
|
||||
"message": "Ana sayfada yapılandır"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "İptal"
|
||||
},
|
||||
"confirmClose": {
|
||||
"message": "Kapat"
|
||||
},
|
||||
"confirmDefault": {
|
||||
"message": "Öntanımlıyı kullan"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Sil"
|
||||
},
|
||||
"confirmDiscardChanges": {
|
||||
"message": "Değişiklikleri iptal et?"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Hayır"
|
||||
},
|
||||
"confirmOK": {
|
||||
"message": "Tamam"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Kaydet"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Dur"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Evet"
|
||||
},
|
||||
"connectingDropbox": {
|
||||
"message": "Dropbox'a bağlanıyor..."
|
||||
},
|
||||
"connectingDropboxNotAllowed": {
|
||||
"message": "Dropbox'a bağlanmak yalnızca doğrudan web mağazasından yüklenen uygulamalarda kullanılabilir"
|
||||
},
|
||||
"copied": {
|
||||
"message": "Kopyalandı"
|
||||
},
|
||||
"copy": {
|
||||
"message": "Kopyala"
|
||||
},
|
||||
"dateInstalled": {
|
||||
"message": "Kurulum tarihi"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"message": "Güncelleme tarihi"
|
||||
},
|
||||
"dbError": {
|
||||
"message": "Stylus veritabanı kullanılırken bir hata oluştu. Olası çözümler içeren bir web sayfasını ziyaret etmek ister misiniz?"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "öntanımlı"
|
||||
"message": "Kontrol ediliyor...",
|
||||
"description": "Text to display when checking a style for an update"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Bu stili silmek istediğinizden emin misiniz?"
|
||||
"message": "Bu stili silmek istediğinizden emin misiniz?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Sil"
|
||||
"message": "Sil",
|
||||
"description": "Label for the button to delete a style"
|
||||
},
|
||||
"description": {
|
||||
"message": "Kullanıcı stil yöneticisi Stylus ile Web'in stilini yenileyin. Stylus Google, Facebook, YouTube, Orkut ve diğer pek çok site için temalar ve görünüm yüklemenize imkân tanır."
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Tüm stilleri kapat"
|
||||
"message": "Kullanıcı stil yöneticisi Stylus ile Web'in stilini yenileyin. Stylus Google, Facebook, YouTube, Orkut ve diğer pek çok site için temalar ve görünüm yüklemenize imkân tanır.",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Devre dışı bırak"
|
||||
},
|
||||
"dragDropMessage": {
|
||||
"message": "İçe aktarmak için yedek dosyanızı bu sayfada herhangi bir yere bırakın."
|
||||
},
|
||||
"dragDropUsercssTabstrip": {
|
||||
"message": "Dosyayı yüklemek için sekme şeridine (sekme başlıklarının gösterildiği alan) bırakın."
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Sil"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Satıra git (veya satır:sütun)"
|
||||
"message": "Devre dışı bırak",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Stili Düzenle"
|
||||
"message": "Stili Düzenle",
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Düzenle"
|
||||
"message": "Düzenle",
|
||||
"description": "Label for the button to go to the edit style page"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "$stylename$ Stilini Düzenleyin",
|
||||
"description": "Title of the page for editing styles",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -238,408 +102,60 @@
|
|||
}
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Etkinleştir"
|
||||
"message": "Etkinleştir",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"excludeStyleByDomainLabel": {
|
||||
"message": "Mevcut alan adını hariç tut"
|
||||
},
|
||||
"excludeStyleByUrlLabel": {
|
||||
"message": "Mevcut URL'yi hariç tut"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Dışa aktar"
|
||||
},
|
||||
"exportSavedSuccess": {
|
||||
"message": "Dosya başarıyla kaydedildi"
|
||||
},
|
||||
"externalFeedback": {
|
||||
"message": "Geribildirim"
|
||||
},
|
||||
"externalHomepage": {
|
||||
"message": "Anasayfa"
|
||||
},
|
||||
"externalLink": {
|
||||
"message": "Harici bağlantı"
|
||||
},
|
||||
"externalSupport": {
|
||||
"message": "Destek"
|
||||
},
|
||||
"externalUsercssDocument": {
|
||||
"message": "Usercss belgeleri"
|
||||
},
|
||||
"filteredStyles": {
|
||||
"message": "Toplam $numTotal$ gösterilen $numShown$",
|
||||
"placeholders": {
|
||||
"numShown": {
|
||||
"content": "$1"
|
||||
},
|
||||
"numTotal": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filteredStylesAllHidden": {
|
||||
"message": "Şu anda uygulanan filtreler hiçbir stille eşleşmiyor"
|
||||
},
|
||||
"findStyles": {
|
||||
"message": "Stil bul"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Ekle"
|
||||
},
|
||||
"genericClone": {
|
||||
"message": "Klon"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Devre dışı"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Etkin"
|
||||
},
|
||||
"genericError": {
|
||||
"message": "Hata"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "Geçmiş"
|
||||
},
|
||||
"genericNext": {
|
||||
"message": "Sonraki"
|
||||
},
|
||||
"genericPrevious": {
|
||||
"message": "Önceki"
|
||||
},
|
||||
"genericResetLabel": {
|
||||
"message": "Yenile"
|
||||
},
|
||||
"genericSavedMessage": {
|
||||
"message": "Kaydedildi"
|
||||
},
|
||||
"genericTitle": {
|
||||
"message": "Başlık"
|
||||
},
|
||||
"genericUnknown": {
|
||||
"message": "Bilinmiyor"
|
||||
},
|
||||
"gettingStyles": {
|
||||
"message": "Tüm stiller alınıyor..."
|
||||
"findStylesForSite": {
|
||||
"message": "Bu site için başka stiller bul",
|
||||
"description": "Text for a link that gets a list of styles for the current site"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Yardım"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Bir komut adı yazın"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Bir kısayol tuşuna basın"
|
||||
},
|
||||
"hostDisabled": {
|
||||
"message": "Bu ana bilgisayar, kullanılan tarayıcının mevcut sürümündeki bir hata nedeniyle devre dışı bırakıldı"
|
||||
},
|
||||
"importAppendLabel": {
|
||||
"message": "Stile ekle"
|
||||
},
|
||||
"importAppendTooltip": {
|
||||
"message": "İçe aktarılan stili geçerli stile ekleme"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "İçe aktar"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Stilin üzerine yaz"
|
||||
},
|
||||
"importReplaceTooltip": {
|
||||
"message": "Geçerli stilin içeriğini atın ve içe aktarılan stil ile üzerine yazın"
|
||||
},
|
||||
"importReportLegendAdded": {
|
||||
"message": "eklendi"
|
||||
},
|
||||
"importReportLegendIdentical": {
|
||||
"message": "Özdeş olan atlandı"
|
||||
},
|
||||
"importReportLegendInvalid": {
|
||||
"message": "geçersiz olan atlandı"
|
||||
},
|
||||
"importReportLegendUpdatedBoth": {
|
||||
"message": "hem meta bilgi hem de kod güncellendi"
|
||||
},
|
||||
"importReportLegendUpdatedCode": {
|
||||
"message": "güncellenmiş kod"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Stillerin içeri aktarılması tamamlandı."
|
||||
},
|
||||
"importReportUnchanged": {
|
||||
"message": "Hiçbir şey değiştirilmedi."
|
||||
},
|
||||
"importReportUndone": {
|
||||
"message": "stiller geri çevrildi"
|
||||
},
|
||||
"importReportUndoneTitle": {
|
||||
"message": "İçe aktarma geri alındı"
|
||||
},
|
||||
"installButton": {
|
||||
"message": "Stil kur"
|
||||
},
|
||||
"installButtonInstalled": {
|
||||
"message": "Stil kuruldu"
|
||||
},
|
||||
"installButtonReinstall": {
|
||||
"message": "Stili yeniden kur"
|
||||
},
|
||||
"installButtonUpdate": {
|
||||
"message": "Stili güncelle"
|
||||
"message": "Yardım",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Güncellemeyi yükle"
|
||||
},
|
||||
"installUpdateFromLabel": {
|
||||
"message": "Güncellemeleri denetle"
|
||||
},
|
||||
"license": {
|
||||
"message": "Lisans"
|
||||
},
|
||||
"linkGetHelp": {
|
||||
"message": "Destek al"
|
||||
},
|
||||
"linkStylusWiki": {
|
||||
"message": "Viki"
|
||||
},
|
||||
"linkTranslate": {
|
||||
"message": "Çevir"
|
||||
},
|
||||
"linterIssues": {
|
||||
"message": "Sorunlar"
|
||||
},
|
||||
"manageFaviconsGray": {
|
||||
"message": "Gri renkte"
|
||||
},
|
||||
"manageFaviconsHelp": {
|
||||
"message": "Stylus harici bir servis kullanır https://icons.duckduckgo.com"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Filtreler"
|
||||
"message": "Güncellemeyi yükle",
|
||||
"description": "Label for the button to install an update for a single style"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Yüklü Stiller"
|
||||
},
|
||||
"manageMaxTargets": {
|
||||
"message": "Uygulanan ögelerin sayısı"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "Yeni yönetim ara yüzü düzeni"
|
||||
},
|
||||
"manageOnlyEnabled": {
|
||||
"message": "Yalnızca etkin stiller"
|
||||
},
|
||||
"manageOnlyLocal": {
|
||||
"message": "Yalnızca yerelde oluşturulmuş stiller"
|
||||
},
|
||||
"manageOnlyLocalTooltip": {
|
||||
"message": "Stiller, bir userstyles.org sayfası üzerinden yülenmemiş."
|
||||
},
|
||||
"manageOnlyUpdates": {
|
||||
"message": "Sadece güncellemeler ya da sorunlar ile"
|
||||
},
|
||||
"menuShowBadge": {
|
||||
"message": "Etkin stil sayısını göster"
|
||||
},
|
||||
"noFileToImport": {
|
||||
"message": "Stillerinizi içe aktarmak için önce dışa aktarmanız gerekir."
|
||||
"message": "Yüklü Stiller",
|
||||
"description": "Heading for the manage page"
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "Bu site için hiçbir stil yüklenmedi."
|
||||
"message": "Bu site için hiçbir stil yüklenmedi.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Yüklü stilleri yönet"
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Seçenekler"
|
||||
},
|
||||
"openStylesManager": {
|
||||
"message": "Stil yöneticisini aç"
|
||||
},
|
||||
"optionsActions": {
|
||||
"message": "İşlemler"
|
||||
},
|
||||
"optionsAdvanced": {
|
||||
"message": "Gelişmiş"
|
||||
},
|
||||
"optionsAdvancedContextDelete": {
|
||||
"message": "Editör bağlam menüsüne 'Sil' seçeneği ekle"
|
||||
},
|
||||
"optionsAdvancedExposeIframes": {
|
||||
"message": "HTML[stylus-iframe] vasıtasıyla iframe'leri gösterin"
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Etkin değilken arkaplan rengi"
|
||||
},
|
||||
"optionsBadgeNormal": {
|
||||
"message": "Arkaplan rengi"
|
||||
},
|
||||
"optionsCheck": {
|
||||
"message": "Stilleri güncelle"
|
||||
},
|
||||
"optionsCheckUpdate": {
|
||||
"message": "Uygun tüm güncellemeleri kontrol et ve yükle."
|
||||
},
|
||||
"optionsCustomizePopup": {
|
||||
"message": "Açılır pencere"
|
||||
},
|
||||
"optionsCustomizeUpdate": {
|
||||
"message": "Güncellemeler"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Seçenekler"
|
||||
},
|
||||
"optionsOpen": {
|
||||
"message": "Aç"
|
||||
},
|
||||
"optionsOpenManager": {
|
||||
"message": "Stilleri yönet"
|
||||
},
|
||||
"optionsPopupWidth": {
|
||||
"message": "Popup genişliği (piksel cinsinden)"
|
||||
},
|
||||
"optionsReset": {
|
||||
"message": "Ayarları varsayılanlara sıfırla."
|
||||
},
|
||||
"optionsResetButton": {
|
||||
"message": "Sıfırlama seçenekleri"
|
||||
},
|
||||
"optionsSubheading": {
|
||||
"message": "Daha Fazla Seçenek"
|
||||
},
|
||||
"optionsSyncConnect": {
|
||||
"message": "Bağlan"
|
||||
},
|
||||
"optionsSyncDisconnect": {
|
||||
"message": "Bağlantıyı kes"
|
||||
},
|
||||
"optionsSyncLogin": {
|
||||
"message": "Giriş yap"
|
||||
},
|
||||
"optionsSyncStatusConnected": {
|
||||
"message": "Bağlandı"
|
||||
},
|
||||
"optionsSyncStatusConnecting": {
|
||||
"message": "Bağlanılıyor..."
|
||||
},
|
||||
"optionsSyncStatusDisconnected": {
|
||||
"message": "Bağlantı kesildi"
|
||||
},
|
||||
"optionsSyncStatusDisconnecting": {
|
||||
"message": "Bağlantı kesiliyor..."
|
||||
},
|
||||
"optionsUpdateImportNote": {
|
||||
"message": "Eski versiyonlardan ya da Stylish'ten stil yedeklerini içeri aktarırken, hepsinin güncel olduğundan emin olmak için stil yöneticisinden bir defa elle güncelleme kontrolü yapın"
|
||||
},
|
||||
"optionsUpdateInterval": {
|
||||
"message": "Saat cinsinden kullanıcı stili otomatik güncelleme aralığı (devre dışı bırakmak için 0 yazın) "
|
||||
},
|
||||
"overwriteFileExport": {
|
||||
"message": "Mevcut bir dosyanın üzerine yazmak istiyor musunuz?"
|
||||
},
|
||||
"paginationCurrent": {
|
||||
"message": "Şimdiki sayfa"
|
||||
},
|
||||
"paginationEstimated": {
|
||||
"message": "Tahmini sayfa sayısı"
|
||||
},
|
||||
"paginationNext": {
|
||||
"message": "Sonraki sayfa"
|
||||
},
|
||||
"paginationPrevious": {
|
||||
"message": "Önceki sayfa"
|
||||
},
|
||||
"paginationTotal": {
|
||||
"message": "Toplam sayfa"
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Şu an açık olan site için aktif olan stil sayısı"
|
||||
},
|
||||
"previewLabel": {
|
||||
"message": "Canlı önizleme"
|
||||
},
|
||||
"readingStyles": {
|
||||
"message": "Stiller okunuyor..."
|
||||
},
|
||||
"replace": {
|
||||
"message": "Değiştir"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Hepsini değiştir"
|
||||
},
|
||||
"replaceWith": {
|
||||
"message": "Şununla değiştir"
|
||||
},
|
||||
"retrieveBckp": {
|
||||
"message": "Stilleri içe aktar"
|
||||
},
|
||||
"retrieveDropboxSync": {
|
||||
"message": "Dropbox'a aktar"
|
||||
},
|
||||
"search": {
|
||||
"message": "Ara"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Regexp araması için /re/ sözdizimini (syntax) kullanın."
|
||||
},
|
||||
"searchResultInstallCount": {
|
||||
"message": "Toplam kurulum sayısı"
|
||||
},
|
||||
"searchResultNoneFound": {
|
||||
"message": "Bu site için stil bulunamadı."
|
||||
},
|
||||
"searchResultWeeklyCount": {
|
||||
"message": "Haftalık kurulum sayısı"
|
||||
"message": "Yüklü stilleri yönet",
|
||||
"description": "Link to open the manage page."
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Başka bölüm ekle"
|
||||
"message": "Başka bölüm ekle",
|
||||
"description": "Label for the button to add a section"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Kod"
|
||||
"message": "Kod",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Bölümü kaldır"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Bölümler"
|
||||
},
|
||||
"shortcuts": {
|
||||
"message": "Kısayollar"
|
||||
},
|
||||
"shortcutsNote": {
|
||||
"message": "Klavye kısayolları tanımla"
|
||||
},
|
||||
"sortDateNewestFirst": {
|
||||
"message": "önce en yeniler"
|
||||
},
|
||||
"sortDateOldestFirst": {
|
||||
"message": "önce en eskiler"
|
||||
},
|
||||
"sortStylesHelpTitle": {
|
||||
"message": "İçeriği sırala"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp geçerli değil."
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Güzelleştir"
|
||||
"message": "Bölümü kaldır",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Yönetim sayfasına dön"
|
||||
"message": "Yönetim sayfasına dön",
|
||||
"description": "Label for cancel button for style editing"
|
||||
},
|
||||
"styleChangesNotSaved": {
|
||||
"message": "Bu stilde yaptığınız, kaydedilmemiş değişiklikler var."
|
||||
"message": "Bu stilde yaptığınız, kaydedilmemiş değişiklikler var.",
|
||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Etkin"
|
||||
},
|
||||
"styleFromMozillaFormatPrompt": {
|
||||
"message": "Mozilla biçemiyle yazılmış kodu yapıştır"
|
||||
"message": "Etkin",
|
||||
"description": "Label for the enabled state of styles"
|
||||
},
|
||||
"styleInstall": {
|
||||
"message": "'$stylename$' Stylus'e yüklensin mi?",
|
||||
"description": "Confirmation when installing a style",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
|
@ -647,84 +163,20 @@
|
|||
}
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Bir ad girin"
|
||||
},
|
||||
"styleMozillaFormatHeading": {
|
||||
"message": "Mozilla Biçemi"
|
||||
},
|
||||
"styleRegexpInvalidExplanation": {
|
||||
"message": "Hiç derlenemeyen bazı 'regexp ()' kuralları."
|
||||
},
|
||||
"styleRegexpProblemTooltip": {
|
||||
"message": "'Regexp ()' yanlış kullanımı nedeniyle uygulanmayan bölüm sayısı"
|
||||
},
|
||||
"styleRegexpTestFull": {
|
||||
"message": "Eşleşen sekmeler"
|
||||
},
|
||||
"styleRegexpTestInvalid": {
|
||||
"message": "Geçersiz regexp'ler atlandı"
|
||||
},
|
||||
"styleRegexpTestNone": {
|
||||
"message": "Eşleşen sekme yok"
|
||||
},
|
||||
"styleRegexpTestPartial": {
|
||||
"message": "Tam olarak eşleşmediğinden atlandı"
|
||||
},
|
||||
"styleRegexpTestTitle": {
|
||||
"message": "Eşleşen açık sekmelerin listesi (sekmesine gitmek için URL'ye tıklayın)"
|
||||
"message": "Bir ad girin",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Kaydet"
|
||||
"message": "Kaydet",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "Kodun Mozilla biçimi, Firefox için Stylish ile kullanılabilir ve userstyles.org sitesine gönderilebilir."
|
||||
},
|
||||
"styleToMozillaFormatTitle": {
|
||||
"message": "Mozilla biçeminde bir stil"
|
||||
},
|
||||
"styleUpdate": {
|
||||
"message": "\";$stylename$\" stilini güncelleştirmek istiyor musunuz?",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus bunun gibi sayfalarda çalışmaz."
|
||||
},
|
||||
"stylusUnavailableForURLdetails": {
|
||||
"message": "Bir güvenlik önlemi olarak tarayıcı eklentilerin, kendinin yerleşik sayfalarını (chrome://version, Chrome 61'den itibaren standart yeni sekme sayfası, about: addons vb.) ve diğer uzantı sayfalarını etkilemesini engeller. Ayrıca her tarayıcı kendi uzantı galerisine (Chrome Web Mağazası veya AMO gibi) erişimi de kısıtlar. "
|
||||
},
|
||||
"syncDropboxDeprecated": {
|
||||
"message": "Dropbox içe/dışa aktarma, seçenekler sayfasında daha gelişmiş bir stil senkronizasyonu ile değiştirilir."
|
||||
},
|
||||
"syncDropboxStyles": {
|
||||
"message": "Dropbox'tan aktar"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Stili değiştir"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Geri al"
|
||||
},
|
||||
"undoGlobal": {
|
||||
"message": "Tüm bölümlerde geri alma işlemi uygula."
|
||||
},
|
||||
"unreachableContentScript": {
|
||||
"message": "Sayfayla iletişim kurulamadı. Sekmeyi yeniden yüklemeyi deneyin."
|
||||
},
|
||||
"unreachableFileHint": {
|
||||
"message": "Stylus, eğer siz Stylus chrome://extensions sayfasından ilgili onay kutusunu işaretlerseniz file:// adreslerine erişebilir."
|
||||
},
|
||||
"unzipStyles": {
|
||||
"message": "Stiller açılıyor..."
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "Güncelleme bulunamadı."
|
||||
"message": "Kodun Mozilla biçimi, Firefox için Stylish ile kullanılabilir ve userstyles.org sitesine gönderilebilir.",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Güncellenemedi: sunucu yanıt olarak $code$ kodunu gönderdi.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"content": "$1"
|
||||
|
@ -732,60 +184,15 @@
|
|||
}
|
||||
},
|
||||
"updateCheckFailServerUnreachable": {
|
||||
"message": "Güncellenemedi: sunucuya erişilemiyor."
|
||||
},
|
||||
"updateCheckHistory": {
|
||||
"message": "Güncelleme kontrolü geçmişi"
|
||||
},
|
||||
"updateCheckManualUpdateForce": {
|
||||
"message": "Güncellemeyi kur (yerel düzenlemelerin üzerine yazılacak)"
|
||||
},
|
||||
"updateCheckManualUpdateHint": {
|
||||
"message": "Bir güncellemeyi zorla yapmak yerel düzenlemelerin üstüne yazacaktır."
|
||||
},
|
||||
"updateCheckSkippedLocallyEdited": {
|
||||
"message": "Bu stil yerel olarak düzenlendi."
|
||||
},
|
||||
"updateCheckSkippedMaybeLocallyEdited": {
|
||||
"message": "Bu stil yerel olarak düzenlenmiş olabilir."
|
||||
"message": "Güncellenemedi: sunucuya erişilemiyor.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Stil güncel."
|
||||
"message": "Stil güncel.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Güncelleme tamamlandı."
|
||||
},
|
||||
"updatesCurrentlyInstalled": {
|
||||
"message": "Kurulan güncellemeler:"
|
||||
},
|
||||
"uploadingFile": {
|
||||
"message": "Dosya Yükleniyor..."
|
||||
},
|
||||
"usercssAvoidOverwriting": {
|
||||
"message": "Mevcut bir stilin üzerine yazmaktan kaçınmak için lütfen @name veya @ ad alanının değerini değiştirin."
|
||||
},
|
||||
"usercssConfigIncomplete": {
|
||||
"message": "Stil, yapılandırma iletişim kutusu gösterildikten sonra güncellendi veya silindi. Bu değişkenler, stilin meta verilerini bozmamak için kaydedilmedi:"
|
||||
},
|
||||
"usercssEditorNamePlaceholder": {
|
||||
"message": "Kodda @name belirtin"
|
||||
},
|
||||
"usercssReplaceTemplateConfirmation": {
|
||||
"message": "Yeni Usercss stilleri için varsayılan şablonu geçerli kodla değiştir?"
|
||||
},
|
||||
"usercssReplaceTemplateSectionBody": {
|
||||
"message": "Kodu buraya ekle..."
|
||||
},
|
||||
"versionInvalidOlder": {
|
||||
"message": "Sürüm, kurulu stillerden daha eski."
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Şunun için stil yaz:"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "bu URL"
|
||||
},
|
||||
"zipStyles": {
|
||||
"message": "Stiller sıkıştırılıyor..."
|
||||
"message": "Güncelleme tamamlandı.",
|
||||
"description": "Text that displays when an update completed"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,285 +0,0 @@
|
|||
{
|
||||
"InaccessibleFileHint": {
|
||||
"message": "Stylus не може отримати доступ до деяких типів файлів (наприклад, файли PDF і JSON)."
|
||||
},
|
||||
"addStyleLabel": {
|
||||
"message": "Написати новий стиль"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Додати стиль"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Прозорість"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Додати"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Застосувати до $applies$",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "та інші"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URL в домені"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Щоб вказати, до яких URL відноситься код в цьому розділі, скористайтеся параметром \"Застосувати до\"."
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Застосувати до"
|
||||
},
|
||||
"appliesLineWidgetLabel": {
|
||||
"message": "Показати цільові сайти секцій"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Видалити"
|
||||
},
|
||||
"appliesRemoveError": {
|
||||
"message": "Не можна видалити останній елемент"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Вказати"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Все"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL, що починаються з "
|
||||
},
|
||||
"applyAllUpdates": {
|
||||
"message": "Застосувати всі оновлення"
|
||||
},
|
||||
"author": {
|
||||
"message": "Автор"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Резервне копіювання"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Експорт стилів"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Перевірка ... "
|
||||
},
|
||||
"clickToUninstall": {
|
||||
"message": "Натисніть, щоб видалити "
|
||||
},
|
||||
"cm_selectByTokensTooltip": {
|
||||
"message": "Приклади токенов: .foo-бар-2 #aabbcc 0,32 !important\nЯкщо вимкнено: вибираються слова, розділені розділовими знаками."
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Скасувати "
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Видалити "
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Ні"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Зберегти"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Зупинити"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Так"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Видалити"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Вимкнути"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Видалити"
|
||||
},
|
||||
"findStyles": {
|
||||
"message": "Знайти стилі"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Додати"
|
||||
},
|
||||
"genericDescription": {
|
||||
"message": "Опис"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Відключено"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Увімкнено"
|
||||
},
|
||||
"genericError": {
|
||||
"message": "Помилка"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "Історія"
|
||||
},
|
||||
"genericNext": {
|
||||
"message": "Наступний "
|
||||
},
|
||||
"genericPrevious": {
|
||||
"message": "Попередній"
|
||||
},
|
||||
"genericResetLabel": {
|
||||
"message": "Скинути"
|
||||
},
|
||||
"genericSavedMessage": {
|
||||
"message": "Збережено"
|
||||
},
|
||||
"genericTitle": {
|
||||
"message": "Ім'я"
|
||||
},
|
||||
"genericUnknown": {
|
||||
"message": "Невідомо"
|
||||
},
|
||||
"gettingStyles": {
|
||||
"message": "Отримання всіх стилів ..."
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Довідка"
|
||||
},
|
||||
"helpKeyMapCommand": {
|
||||
"message": "Введіть ім'я команди"
|
||||
},
|
||||
"helpKeyMapHotkey": {
|
||||
"message": "Натисніть клавішу"
|
||||
},
|
||||
"hostDisabled": {
|
||||
"message": "Цей хост вимкнено через помилку в поточній версії браузера, що використовується"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Імпорт"
|
||||
},
|
||||
"importReplaceLabel": {
|
||||
"message": "Замінити стиль"
|
||||
},
|
||||
"importReportLegendIdentical": {
|
||||
"message": "ідентичні пропущені"
|
||||
},
|
||||
"importReportLegendInvalid": {
|
||||
"message": "недісних пропущено"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Імпорт стилів закінчено"
|
||||
},
|
||||
"installUpdate": {
|
||||
"message": "Встановити оновлення"
|
||||
},
|
||||
"manageFavicons": {
|
||||
"message": "Піктограми для цільових сайтів"
|
||||
},
|
||||
"manageFilters": {
|
||||
"message": "Фільтри"
|
||||
},
|
||||
"manageHeading": {
|
||||
"message": "Встановити Styles"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "Новий макет інтерфейсу управління користувача"
|
||||
},
|
||||
"meta_unknownJSONLiteral": {
|
||||
"message": "Невірний JSON: $literal$не є дійсним літералом JSON",
|
||||
"placeholders": {
|
||||
"literal": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"openManage": {
|
||||
"message": "Керування"
|
||||
},
|
||||
"openOptions": {
|
||||
"message": "Налаштування"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Налаштування"
|
||||
},
|
||||
"optionsReset": {
|
||||
"message": "Скидання налаштувань до значень за замовчуванням"
|
||||
},
|
||||
"optionsResetButton": {
|
||||
"message": "Скинути параметри"
|
||||
},
|
||||
"optionsSubheading": {
|
||||
"message": "Додатково"
|
||||
},
|
||||
"optionsSyncLogin": {
|
||||
"message": "Логін"
|
||||
},
|
||||
"optionsSyncStatusRelogin": {
|
||||
"message": "Сеанс закінчився, будь ласка, увійдіть ще раз."
|
||||
},
|
||||
"paginationNext": {
|
||||
"message": "Наступна сторінка"
|
||||
},
|
||||
"paginationPrevious": {
|
||||
"message": "Попередня сторінка"
|
||||
},
|
||||
"replace": {
|
||||
"message": "Замінити"
|
||||
},
|
||||
"replaceAll": {
|
||||
"message": "Замінити все"
|
||||
},
|
||||
"retrieveBckp": {
|
||||
"message": "Імпорт стилів"
|
||||
},
|
||||
"search": {
|
||||
"message": "Пошук"
|
||||
},
|
||||
"searchStylesAll": {
|
||||
"message": "Усі"
|
||||
},
|
||||
"searchStylesCode": {
|
||||
"message": "CSS код"
|
||||
},
|
||||
"searchStylesHelp": {
|
||||
"message": "</> або <Ctrl-F>клавіша фокусує поле пошуку.\nРежим за замовчуванням — це пошук у звичайному тексті для всіх термінів, розділених пробілами, у будь-якому порядку.\nТочні слова: оберніть запит у подвійні лапки, напр. <.header ~ div\">\nРегулярні вирази: включають косі риски та прапорці, напр.</body.*?\\ba\\b/i>\n«By URL» в селекторі області: знаходить стилі, які застосовуються до повністю вказаної URL-адреси, напр. https://www.example.org/\n\"Metadata\" в селектрі області: шукає в іменах, \"applies to\" специфікаторів, URL-адреси встановлення, URL-адресі оновлення та всьому блоку метаданих для стилів CSS користувача."
|
||||
},
|
||||
"searchStylesName": {
|
||||
"message": "Назва"
|
||||
},
|
||||
"sectionCode": {
|
||||
"message": "Код"
|
||||
},
|
||||
"sections": {
|
||||
"message": "Розділи"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Облагородити"
|
||||
},
|
||||
"styleCancelEditLabel": {
|
||||
"message": "Повернутися до керування"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Увімкнено"
|
||||
},
|
||||
"stylePreferSchemeLabel": {
|
||||
"message": "Темна/Світла тема"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Зберегти"
|
||||
},
|
||||
"syncErrorRelogin": {
|
||||
"message": "Помилка синхронізації. Ви вийшли із системи. \nСпробуйте повторно увійти до системи в налаштуваннях Stylus."
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Включити/виключити стиль"
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Створити стиль для:"
|
||||
},
|
||||
"writeStyleForURL": {
|
||||
"message": "цей URL"
|
||||
},
|
||||
"zipStyles": {
|
||||
"message": "Запаковування стилів ... "
|
||||
}
|
||||
}
|
|
@ -1,379 +0,0 @@
|
|||
{
|
||||
"InaccessibleFileHint": {
|
||||
"message": "Stylus không thể truy cập một số kiểu tập tin (chẳng hạn như PDF và JSON)."
|
||||
},
|
||||
"addStyleLabel": {
|
||||
"message": "Viết bảng định kiểu mới"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Thêm bảng định kiểu"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Độ mờ"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Thêm"
|
||||
},
|
||||
"appliesDisplay": {
|
||||
"message": "Áp dụng với: $applies$",
|
||||
"placeholders": {
|
||||
"applies": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "và một số khác"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "Các địa chỉ thuộc tên miền này"
|
||||
},
|
||||
"appliesHelp": {
|
||||
"message": "Dùng tuỳ chọn \"Áp dụng với\" để giới hạn các địa chỉ cho đoạn mã này"
|
||||
},
|
||||
"appliesLabel": {
|
||||
"message": "Áp dụng với"
|
||||
},
|
||||
"appliesLineWidgetLabel": {
|
||||
"message": "Hiển thị thông tin \"Áp dụng với\""
|
||||
},
|
||||
"appliesLineWidgetWarning": {
|
||||
"message": "Không hoạt động với CSS tối giản"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URL khớp với biểu thức chính quy"
|
||||
},
|
||||
"appliesRemove": {
|
||||
"message": "Xoá"
|
||||
},
|
||||
"appliesRemoveError": {
|
||||
"message": "Không thể xoá mục \"Áp dụng với\" cuối cùng"
|
||||
},
|
||||
"appliesSpecify": {
|
||||
"message": "Chỉ định"
|
||||
},
|
||||
"appliesToEverything": {
|
||||
"message": "Tất cả"
|
||||
},
|
||||
"appliesUrlPrefixOption": {
|
||||
"message": "URL bắt đầu bằng"
|
||||
},
|
||||
"author": {
|
||||
"message": "Tác giả"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Sao lưu"
|
||||
},
|
||||
"backupMessage": {
|
||||
"message": "Để nhập tập tin sao lưu, kéo và thả nó vào trang này hoặc nhấp vào nút Nhập.\n\nĐể xuất một bản sao lưu tương thích với Stylus trước phiên bản 1.5.18, nhấp chuột phải hoặc nhấn giữ Shift khi nhấp chuột trái vào nút Xuất."
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Xuất bảng định kiểu"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Kiểm tra bản cập nhật mới"
|
||||
},
|
||||
"checkingForUpdate": {
|
||||
"message": "Đang kiểm tra..."
|
||||
},
|
||||
"clickToUninstall": {
|
||||
"message": "Nhấp để huỷ kích hoạt"
|
||||
},
|
||||
"cm_autoCloseBrackets": {
|
||||
"message": "Tự động đóng ngoặc và nháy"
|
||||
},
|
||||
"cm_autoCloseBracketsTooltip": {
|
||||
"message": "Tự động thêm dấu đóng tương ứng khi nhập một trong các dấu (, [, {, ' và \"."
|
||||
},
|
||||
"cm_autocompleteOnTyping": {
|
||||
"message": "Tự động hoàn thành"
|
||||
},
|
||||
"cm_colorpicker": {
|
||||
"message": "Bộ chọn màu cho màu CSS"
|
||||
},
|
||||
"cm_indentWithTabs": {
|
||||
"message": "Lùi đầu dòng thông minh bằng tab"
|
||||
},
|
||||
"cm_lineWrapping": {
|
||||
"message": "Gập dòng dài"
|
||||
},
|
||||
"cm_linter": {
|
||||
"message": "Trình phân tích cú pháp"
|
||||
},
|
||||
"cm_matchHighlight": {
|
||||
"message": "Làm nổi"
|
||||
},
|
||||
"cm_matchHighlightSelection": {
|
||||
"message": "Chỉ vùng được chọn"
|
||||
},
|
||||
"cm_matchHighlightToken": {
|
||||
"message": "Token nằm dưới con trỏ văn bản"
|
||||
},
|
||||
"cm_selectByTokens": {
|
||||
"message": "Nhấp đúp để chọn token"
|
||||
},
|
||||
"cm_selectByTokensTooltip": {
|
||||
"message": "Ví dụ về token: .foo-bar-2 #aabbcc 0.32 !important\nKhi tắt: Chọn từ (phân tách bằng dấu câu)."
|
||||
},
|
||||
"cm_smartIndent": {
|
||||
"message": "Lùi đầu dòng thông minh"
|
||||
},
|
||||
"cm_tabSize": {
|
||||
"message": "Chiều rộng tab"
|
||||
},
|
||||
"cm_theme": {
|
||||
"message": "Chủ đề"
|
||||
},
|
||||
"colorpickerSwitchFormatTooltip": {
|
||||
"message": "Đổi định dạng: HEX → RGB → HSL.\nNhấn giữ phím Shift khi nhấp để đảo thứ tự.\nPhím tắt: PgUp và PgDn."
|
||||
},
|
||||
"colorpickerTooltip": {
|
||||
"message": "Mở bộ chọn màu"
|
||||
},
|
||||
"configOnChange": {
|
||||
"message": "khi thay đổi"
|
||||
},
|
||||
"configOnChangeTooltip": {
|
||||
"message": "Tự động lưu và áp dụng"
|
||||
},
|
||||
"configureStyle": {
|
||||
"message": "Thiết lập"
|
||||
},
|
||||
"configureStyleOnHomepage": {
|
||||
"message": "Thiết lập trên trang chủ"
|
||||
},
|
||||
"confirmCancel": {
|
||||
"message": "Huỷ"
|
||||
},
|
||||
"confirmClose": {
|
||||
"message": "Đóng"
|
||||
},
|
||||
"confirmDefault": {
|
||||
"message": "Dùng mặc định"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Xoá"
|
||||
},
|
||||
"confirmDiscardChanges": {
|
||||
"message": "Huỷ thay đổi?"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "Không"
|
||||
},
|
||||
"confirmSave": {
|
||||
"message": "Lưu"
|
||||
},
|
||||
"confirmStop": {
|
||||
"message": "Dừng"
|
||||
},
|
||||
"confirmYes": {
|
||||
"message": "Có"
|
||||
},
|
||||
"connectingDropbox": {
|
||||
"message": "Đang kết nối với Dropbox..."
|
||||
},
|
||||
"connectingDropboxNotAllowed": {
|
||||
"message": "Tính năng kết nối với Dropbox chỉ khả dụng khi cài ứng dụng trực tiếp từ cửa hàng web"
|
||||
},
|
||||
"copied": {
|
||||
"message": "Đã chép vào khay nhớ tạm"
|
||||
},
|
||||
"copy": {
|
||||
"message": "Chép vào khay nhớ tạm"
|
||||
},
|
||||
"dateAbbrDay": {
|
||||
"message": "$value$ ngày",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAbbrHour": {
|
||||
"message": "$value$ giờ",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAbbrMonth": {
|
||||
"message": "$value$ tháng",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAbbrYear": {
|
||||
"message": "$value$ năm",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateInstalled": {
|
||||
"message": "Ngày cài đặt"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"message": "Ngày cập nhật"
|
||||
},
|
||||
"defaultTheme": {
|
||||
"message": "mặc định"
|
||||
},
|
||||
"deleteStyleConfirm": {
|
||||
"message": "Bạn có chắc chắn muốn xoá bảng định kiểu này không?"
|
||||
},
|
||||
"deleteStyleLabel": {
|
||||
"message": "Xoá"
|
||||
},
|
||||
"disableAllStyles": {
|
||||
"message": "Tắt tất cả bảng định kiểu"
|
||||
},
|
||||
"disableAllStylesOff": {
|
||||
"message": "Bật tất cả bảng định kiểu"
|
||||
},
|
||||
"disableStyleLabel": {
|
||||
"message": "Vô hiệu hoá"
|
||||
},
|
||||
"draftAction": {
|
||||
"message": "Chọn \"Có\" để tải bản nháp hoặc \"Không\" để xoá nó đi."
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Xoá"
|
||||
},
|
||||
"editGotoLine": {
|
||||
"message": "Đi đến dòng (hoặc dòng:cột)"
|
||||
},
|
||||
"editStyleHeading": {
|
||||
"message": "Sửa bảng định kiểu"
|
||||
},
|
||||
"editStyleLabel": {
|
||||
"message": "Sửa"
|
||||
},
|
||||
"editStyleTitle": {
|
||||
"message": "Sửa bảng định kiểu $stylename$",
|
||||
"placeholders": {
|
||||
"stylename": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editorSettings": {
|
||||
"message": "Cài đặt trình soạn thảo"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Kích hoạt"
|
||||
},
|
||||
"exportCompatible": {
|
||||
"message": "Xuất (chế độ tương thích)"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Xuất"
|
||||
},
|
||||
"exportSavedSuccess": {
|
||||
"message": "Lưu thành công"
|
||||
},
|
||||
"externalFeedback": {
|
||||
"message": "Phản hồi"
|
||||
},
|
||||
"externalHomepage": {
|
||||
"message": "Trang chủ"
|
||||
},
|
||||
"externalLink": {
|
||||
"message": "Liên kết ngoài"
|
||||
},
|
||||
"externalSupport": {
|
||||
"message": "Ủng hộ"
|
||||
},
|
||||
"genericAdd": {
|
||||
"message": "Thêm"
|
||||
},
|
||||
"genericDescription": {
|
||||
"message": "Mô tả"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Vô hiệu hoá"
|
||||
},
|
||||
"genericEnabledLabel": {
|
||||
"message": "Kích hoạt"
|
||||
},
|
||||
"genericError": {
|
||||
"message": "Lỗi"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "Lịch sử"
|
||||
},
|
||||
"genericNext": {
|
||||
"message": "Sau"
|
||||
},
|
||||
"genericPrevious": {
|
||||
"message": "Trước"
|
||||
},
|
||||
"genericResetLabel": {
|
||||
"message": "Đặt lại"
|
||||
},
|
||||
"genericSavedMessage": {
|
||||
"message": "Đã lưu"
|
||||
},
|
||||
"genericTest": {
|
||||
"message": "Thứ"
|
||||
},
|
||||
"genericTitle": {
|
||||
"message": "Tiêu đề"
|
||||
},
|
||||
"genericUnknown": {
|
||||
"message": "Không xác định"
|
||||
},
|
||||
"helpAlt": {
|
||||
"message": "Trợ giúp"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Nhập"
|
||||
},
|
||||
"importReportLegendAdded": {
|
||||
"message": "đã thêm"
|
||||
},
|
||||
"importReportLegendIdentical": {
|
||||
"message": "Đã bỏ qua các tệp trùng lặp"
|
||||
},
|
||||
"importReportLegendInvalid": {
|
||||
"message": "Đã bỏ qua các tệp không hợp lệ"
|
||||
},
|
||||
"importReportLegendUpdatedBoth": {
|
||||
"message": "đã cập nhật siêu thông tin và mã"
|
||||
},
|
||||
"importReportLegendUpdatedCode": {
|
||||
"message": "đã cập nhật mã"
|
||||
},
|
||||
"importReportLegendUpdatedMeta": {
|
||||
"message": "đã cập nhật siêu thông tin"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Đã nhập xong"
|
||||
},
|
||||
"installUpdateFromLabel": {
|
||||
"message": "Kiểm tra bản cập nhật mới"
|
||||
},
|
||||
"license": {
|
||||
"message": "Giấy phép"
|
||||
},
|
||||
"linterCSSLintIncompatible": {
|
||||
"message": "CSSLint không hỗ trợ bộ tiền xử lý $preprocessorname$",
|
||||
"placeholders": {
|
||||
"preprocessorname": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"linterJSONError": {
|
||||
"message": "Định dạng JSON không hợp lệ"
|
||||
},
|
||||
"styleEnabledLabel": {
|
||||
"message": "Kích hoạt"
|
||||
},
|
||||
"styleSaveLabel": {
|
||||
"message": "Lưu"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,26 +1,169 @@
|
|||
/* global createWorkerApi */// worker-util.js
|
||||
/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */
|
||||
'use strict';
|
||||
|
||||
/** @namespace BackgroundWorker */
|
||||
createWorkerApi({
|
||||
importScripts('/js/worker-util.js');
|
||||
const {loadScript, createAPI} = workerUtil;
|
||||
|
||||
async compileUsercss(...args) {
|
||||
require(['/js/usercss-compiler']); /* global compileUsercss */
|
||||
return compileUsercss(...args);
|
||||
createAPI({
|
||||
parseMozFormat(arg) {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
return parseMozFormat(arg);
|
||||
},
|
||||
compileUsercss,
|
||||
parseUsercssMeta(text, indexOffset = 0) {
|
||||
loadScript(
|
||||
'/js/polyfill.js',
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
);
|
||||
return metaParser.parse(text, indexOffset);
|
||||
},
|
||||
|
||||
nullifyInvalidVars(vars) {
|
||||
require(['/js/meta-parser']); /* global metaParser */
|
||||
loadScript(
|
||||
'/js/polyfill.js',
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
);
|
||||
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);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
function compileUsercss(preprocessor, code, vars) {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
const builder = getUsercssCompiler(preprocessor);
|
||||
vars = simpleVars(vars);
|
||||
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
|
||||
.then(code => parseMozFormat({code}))
|
||||
.then(({sections, errors}) => {
|
||||
if (builder.postprocess) {
|
||||
builder.postprocess(sections, vars);
|
||||
}
|
||||
return {sections, errors};
|
||||
});
|
||||
|
||||
function simpleVars(vars) {
|
||||
if (!vars) {
|
||||
return {};
|
||||
}
|
||||
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
|
||||
// need to test each va's default value.
|
||||
return Object.keys(vars).reduce((output, key) => {
|
||||
const va = vars[key];
|
||||
output[key] = Object.assign({}, va, {
|
||||
value: va.value === null || va.value === undefined ?
|
||||
getVarValue(va, 'default') : getVarValue(va, 'value')
|
||||
});
|
||||
return output;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getVarValue(va, prop) {
|
||||
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
|
||||
// TODO: handle customized image
|
||||
return va.options.find(o => o.name === va[prop]).value;
|
||||
}
|
||||
if ((va.type === 'number' || va.type === 'range') && va.units) {
|
||||
return va[prop] + va.units;
|
||||
}
|
||||
return va[prop];
|
||||
}
|
||||
}
|
||||
|
||||
function getUsercssCompiler(preprocessor) {
|
||||
const BUILDER = {
|
||||
default: {
|
||||
postprocess(sections, vars) {
|
||||
loadScript('/js/sections-util.js');
|
||||
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
|
||||
if (!varDef) return;
|
||||
varDef = ':root {\n' + varDef + '}\n';
|
||||
for (const section of sections) {
|
||||
if (!styleCodeEmpty(section.code)) {
|
||||
section.code = varDef + section.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stylus: {
|
||||
preprocess(source, vars) {
|
||||
loadScript('/vendor/stylus-lang-bundle/stylus.min.js');
|
||||
return new Promise((resolve, reject) => {
|
||||
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
|
||||
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
|
||||
self.stylus(varDef + source).render((err, output) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(output);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
less: {
|
||||
preprocess(source, vars) {
|
||||
if (!self.less) {
|
||||
self.less = {
|
||||
logLevel: 0,
|
||||
useFileCache: false,
|
||||
};
|
||||
}
|
||||
loadScript('/vendor/less-bundle/less.min.js');
|
||||
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
|
||||
return self.less.render(varDefs + source)
|
||||
.then(({css}) => css);
|
||||
}
|
||||
},
|
||||
uso: {
|
||||
preprocess(source, vars) {
|
||||
loadScript('/vendor-overwrites/colorpicker/colorconverter.js');
|
||||
const pool = new Map();
|
||||
return Promise.resolve(doReplace(source));
|
||||
|
||||
function getValue(name, rgb) {
|
||||
if (!vars.hasOwnProperty(name)) {
|
||||
if (name.endsWith('-rgb')) {
|
||||
return getValue(name.slice(0, -4), true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (rgb) {
|
||||
if (vars[name].type === 'color') {
|
||||
const color = colorConverter.parse(vars[name].value);
|
||||
if (!color) return null;
|
||||
const {r, g, b} = color;
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
||||
// prevent infinite recursion
|
||||
pool.set(name, '');
|
||||
return doReplace(vars[name].value);
|
||||
}
|
||||
return vars[name].value;
|
||||
}
|
||||
|
||||
function doReplace(text) {
|
||||
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
|
||||
if (!pool.has(name)) {
|
||||
const value = getValue(name);
|
||||
pool.set(name, value === null ? match : value);
|
||||
}
|
||||
return pool.get(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (preprocessor) {
|
||||
if (!BUILDER[preprocessor]) {
|
||||
throw new Error('unknwon preprocessor');
|
||||
}
|
||||
return BUILDER[preprocessor];
|
||||
}
|
||||
return BUILDER.default;
|
||||
}
|
||||
|
|
|
@ -1,213 +1,396 @@
|
|||
/* 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
|
||||
/* global download prefs openURL FIREFOX CHROME VIVALDI
|
||||
debounce URLS ignoreChromeError getTab
|
||||
styleManager msg navigatorUtil iconUtil workerUtil contentScripts */
|
||||
'use strict';
|
||||
|
||||
//#region API
|
||||
// eslint-disable-next-line no-var
|
||||
var backgroundWorker = workerUtil.createWorker({
|
||||
url: '/background/background-worker.js'
|
||||
});
|
||||
|
||||
addAPI(/** @namespace API */ {
|
||||
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||
deleteStyle: styleManager.deleteStyle,
|
||||
editSave: styleManager.editSave,
|
||||
findStyle: styleManager.findStyle,
|
||||
getAllStyles: styleManager.getAllStyles, // used by importer
|
||||
getSectionsByUrl: styleManager.getSectionsByUrl,
|
||||
getStyle: styleManager.get,
|
||||
getStylesByUrl: styleManager.getStylesByUrl,
|
||||
importStyle: styleManager.importStyle,
|
||||
importManyStyles: styleManager.importMany,
|
||||
installStyle: styleManager.installStyle,
|
||||
styleExists: styleManager.styleExists,
|
||||
toggleStyle: styleManager.toggleStyle,
|
||||
|
||||
/** 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;
|
||||
},
|
||||
}))(),
|
||||
addInclusion: styleManager.addInclusion,
|
||||
removeInclusion: styleManager.removeInclusion,
|
||||
addExclusion: styleManager.addExclusion,
|
||||
removeExclusion: styleManager.removeExclusion,
|
||||
|
||||
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;
|
||||
}
|
||||
download(msg) {
|
||||
delete msg.method;
|
||||
return download(msg.url, msg);
|
||||
},
|
||||
parseCss({code}) {
|
||||
return backgroundWorker.parseMozFormat({code});
|
||||
},
|
||||
getPrefs: prefs.getAll,
|
||||
|
||||
openEditor,
|
||||
|
||||
updateIconBadge(count) {
|
||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||
// so we do a force update which doesn't use the cache.
|
||||
if (CHROME && CHROME < 2661 && this.sender.tab.url.startsWith(URLS.ownOrigin)) {
|
||||
updateIconBadgeForce(this.sender.tab.id, count);
|
||||
} else {
|
||||
updateIconBadge(this.sender.tab.id, count);
|
||||
}
|
||||
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;
|
||||
return true;
|
||||
},
|
||||
|
||||
/** @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
|
||||
},
|
||||
// exposed for stuff that requires followup sendMessage() like popup::openSettings
|
||||
// that would fail otherwise if another extension forced the tab to open
|
||||
// in the foreground thus auto-closing the popup (in Chrome)
|
||||
openURL,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
optionsCustomizeHotkeys() {
|
||||
return browser.runtime.openOptionsPage()
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region Events
|
||||
// eslint-disable-next-line no-var
|
||||
var browserCommands, contextMenus;
|
||||
|
||||
const browserCommands = {
|
||||
openManage: () => API.openManage(),
|
||||
openOptions: () => API.openManage({options: true}),
|
||||
reload: () => chrome.runtime.reload(),
|
||||
// *************************************************************************
|
||||
// register all listeners
|
||||
msg.on(onRuntimeMessage);
|
||||
|
||||
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
|
||||
if (type === 'committed') {
|
||||
// styles would be updated when content script is injected.
|
||||
return;
|
||||
}
|
||||
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId})
|
||||
.catch(msg.ignoreError);
|
||||
});
|
||||
|
||||
if (FIREFOX) {
|
||||
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
|
||||
navigatorUtil.onCommitted(webNavUsercssInstallerFF, {
|
||||
url: [
|
||||
{pathSuffix: '.user.css'},
|
||||
{pathSuffix: '.user.styl'},
|
||||
]
|
||||
});
|
||||
// FF misses some about:blank iframes so we inject our content script explicitly
|
||||
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
|
||||
url: [
|
||||
{urlEquals: 'about:blank'},
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (chrome.contextMenus) {
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
||||
contextMenus[info.menuItemId].click(info, tab));
|
||||
}
|
||||
|
||||
if (chrome.commands) {
|
||||
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
|
||||
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||
}
|
||||
|
||||
const tabIcons = new Map();
|
||||
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
|
||||
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed));
|
||||
|
||||
prefs.subscribe([
|
||||
'disableAll',
|
||||
'badgeDisabled',
|
||||
'badgeNormal',
|
||||
], () => debounce(refreshIconBadgeColor));
|
||||
|
||||
prefs.subscribe([
|
||||
'show-badge'
|
||||
], () => debounce(refreshIconBadgeText));
|
||||
|
||||
prefs.subscribe([
|
||||
'disableAll',
|
||||
'iconset',
|
||||
], () => debounce(refreshAllIcons));
|
||||
|
||||
prefs.initializing.then(() => {
|
||||
refreshIconBadgeColor();
|
||||
refreshAllIconsBadgeText();
|
||||
refreshAllIcons();
|
||||
});
|
||||
|
||||
navigatorUtil.onUrlChange(({tabId, frameId, transitionQualifiers}, type) => {
|
||||
if (type === 'committed' && !frameId) {
|
||||
// it seems that the tab icon would be reset by navigation. We
|
||||
// invalidate the cache here so it would be refreshed by `apply.js`.
|
||||
tabIcons.delete(tabId);
|
||||
|
||||
// however, if the tab was swapped in by forward/backward buttons,
|
||||
// `apply.js` doesn't notify the background to update the icon,
|
||||
// so we have to refresh it manually.
|
||||
if (transitionQualifiers.includes('forward_back')) {
|
||||
msg.sendTab(tabId, {method: 'updateCount'}).catch(msg.ignoreError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
chrome.runtime.onInstalled.addListener(({reason}) => {
|
||||
// save install type: "admin", "development", "normal", "sideload" or "other"
|
||||
// "normal" = addon installed from webstore
|
||||
chrome.management.getSelf(info => {
|
||||
localStorage.installType = info.installType;
|
||||
});
|
||||
|
||||
if (reason !== 'update') return;
|
||||
// translations may change
|
||||
localStorage.L10N = JSON.stringify({
|
||||
browserUIlanguage: chrome.i18n.getUILanguage(),
|
||||
});
|
||||
// themes may change
|
||||
delete localStorage.codeMirrorThemes;
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
// browser commands
|
||||
browserCommands = {
|
||||
openManage() {
|
||||
openURL({url: 'manage.html'});
|
||||
},
|
||||
styleDisableAll(info) {
|
||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||
},
|
||||
};
|
||||
|
||||
if (chrome.commands) {
|
||||
chrome.commands.onCommand.addListener(id => browserCommands[id]());
|
||||
// *************************************************************************
|
||||
// context menus
|
||||
contextMenus = {
|
||||
'show-badge': {
|
||||
title: 'menuShowBadge',
|
||||
click: info => prefs.set(info.menuItemId, info.checked),
|
||||
},
|
||||
'disableAll': {
|
||||
title: 'disableAllStyles',
|
||||
click: browserCommands.styleDisableAll,
|
||||
},
|
||||
'open-manager': {
|
||||
title: 'openStylesManager',
|
||||
click: browserCommands.openManage,
|
||||
},
|
||||
'editor.contextDelete': {
|
||||
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
|
||||
title: 'editDeleteText',
|
||||
type: 'normal',
|
||||
contexts: ['editable'],
|
||||
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
||||
click: (info, tab) => {
|
||||
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension');
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if (chrome.contextMenus) {
|
||||
const createContextMenus = ids => {
|
||||
for (const id of ids) {
|
||||
let item = contextMenus[id];
|
||||
if (item.presentIf && !item.presentIf()) {
|
||||
continue;
|
||||
}
|
||||
item = Object.assign({id}, item);
|
||||
delete item.presentIf;
|
||||
item.title = chrome.i18n.getMessage(item.title);
|
||||
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
|
||||
item.type = 'checkbox';
|
||||
item.checked = prefs.get(id);
|
||||
}
|
||||
if (!item.contexts) {
|
||||
item.contexts = ['browser_action'];
|
||||
}
|
||||
delete item.click;
|
||||
chrome.contextMenus.create(item, ignoreChromeError);
|
||||
}
|
||||
};
|
||||
|
||||
// circumvent the bug with disabling check marks in Chrome 62-64
|
||||
const toggleCheckmark = CHROME >= 3172 && CHROME <= 3288 ?
|
||||
(id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) :
|
||||
((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError));
|
||||
|
||||
const togglePresence = (id, checked) => {
|
||||
if (checked) {
|
||||
createContextMenus([id]);
|
||||
} else {
|
||||
chrome.contextMenus.remove(id, ignoreChromeError);
|
||||
}
|
||||
};
|
||||
|
||||
const keys = Object.keys(contextMenus);
|
||||
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark);
|
||||
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
|
||||
createContextMenus(keys);
|
||||
}
|
||||
|
||||
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) {}
|
||||
// reinject content scripts when the extension is reloaded/updated. Firefox
|
||||
// would handle this automatically.
|
||||
if (!FIREFOX) {
|
||||
setTimeout(contentScripts.injectToAllTabs, 0);
|
||||
}
|
||||
|
||||
// register hotkeys
|
||||
if (FIREFOX && browser.commands && browser.commands.update) {
|
||||
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
|
||||
prefs.subscribe(hotkeyPrefs, (name, value) => {
|
||||
try {
|
||||
name = name.split('.')[1];
|
||||
if (value.trim()) {
|
||||
browser.commands.update({name, shortcut: value});
|
||||
} else {
|
||||
browser.commands.reset(name);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
msg.broadcastTab({method: 'backgroundReady'});
|
||||
|
||||
function webNavUsercssInstallerFF(data) {
|
||||
const {tabId} = data;
|
||||
Promise.all([
|
||||
msg.sendTab(tabId, {method: 'ping'})
|
||||
.catch(() => false),
|
||||
// we need tab index to open the installer next to the original one
|
||||
// and also to skip the double-invocation in FF which assigns tab url later
|
||||
getTab(tabId),
|
||||
]).then(([pong, tab]) => {
|
||||
if (pong !== true && tab.url !== 'about:blank') {
|
||||
window.API_METHODS.openUsercssInstallPage({direct: true}, {tab});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function webNavIframeHelperFF({tabId, frameId}) {
|
||||
if (!frameId) return;
|
||||
msg.sendTab(tabId, {method: 'ping'}, {frameId})
|
||||
.catch(() => false)
|
||||
.then(pong => {
|
||||
if (pong) return;
|
||||
// insert apply.js to iframe
|
||||
const files = chrome.runtime.getManifest().content_scripts[0].js;
|
||||
for (const file of files) {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
frameId,
|
||||
file,
|
||||
matchAboutBlank: true,
|
||||
}, ignoreChromeError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateIconBadge(tabId, count) {
|
||||
let tabIcon = tabIcons.get(tabId);
|
||||
if (!tabIcon) tabIcons.set(tabId, (tabIcon = {}));
|
||||
if (tabIcon.count === count) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
const oldCount = tabIcon.count;
|
||||
tabIcon.count = count;
|
||||
refreshIconBadgeText(tabId, tabIcon);
|
||||
if (Boolean(oldCount) !== Boolean(count)) {
|
||||
refreshIcon(tabId, tabIcon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
function updateIconBadgeForce(tabId, count) {
|
||||
refreshIconBadgeText(tabId, {count});
|
||||
refreshIcon(tabId, {count});
|
||||
}
|
||||
|
||||
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'});
|
||||
});
|
||||
function refreshIconBadgeText(tabId, icon) {
|
||||
iconUtil.setBadgeText({
|
||||
text: prefs.get('show-badge') && icon.count ? String(icon.count) : '',
|
||||
tabId
|
||||
});
|
||||
}
|
||||
|
||||
function refreshIcon(tabId, icon) {
|
||||
const disableAll = prefs.get('disableAll');
|
||||
const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
|
||||
const postfix = disableAll ? 'x' : !icon.count ? 'w' : '';
|
||||
const iconType = iconset + postfix;
|
||||
|
||||
if (icon.iconType === iconType) {
|
||||
return;
|
||||
}
|
||||
icon.iconType = iconset + postfix;
|
||||
const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
|
||||
iconUtil.setIcon({
|
||||
path: sizes.reduce(
|
||||
(obj, size) => {
|
||||
obj[size] = `/images/icon/${iconset}${size}${postfix}.png`;
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
),
|
||||
tabId
|
||||
});
|
||||
}
|
||||
|
||||
function refreshIconBadgeColor() {
|
||||
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
|
||||
iconUtil.setBadgeBackgroundColor({
|
||||
color
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAllIcons() {
|
||||
for (const [tabId, icon] of tabIcons) {
|
||||
refreshIcon(tabId, icon);
|
||||
}
|
||||
refreshIcon(null, {}); // default icon
|
||||
}
|
||||
|
||||
function refreshAllIconsBadgeText() {
|
||||
for (const [tabId, icon] of tabIcons) {
|
||||
refreshIconBadgeText(tabId, icon);
|
||||
}
|
||||
}
|
||||
|
||||
function onRuntimeMessage(msg, sender) {
|
||||
if (msg.method !== 'invokeAPI') {
|
||||
return;
|
||||
}
|
||||
const fn = window.API_METHODS[msg.name];
|
||||
if (!fn) {
|
||||
throw new Error(`unknown API: ${msg.name}`);
|
||||
}
|
||||
const context = {msg, sender};
|
||||
return fn.apply(context, msg.args);
|
||||
}
|
||||
|
||||
// FIXME: popup.js also open editor but it doesn't use this API.
|
||||
function openEditor({id}) {
|
||||
let url = '/edit.html';
|
||||
if (id) {
|
||||
url += `?id=${id}`;
|
||||
}
|
||||
if (chrome.windows && prefs.get('openEditInWindow')) {
|
||||
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
||||
} else {
|
||||
openURL({url});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,30 +1,21 @@
|
|||
/* global bgReady */// common.js
|
||||
/* global msg */
|
||||
/* global URLS ignoreChromeError */// toolbox.js
|
||||
/* global msg queryTabs ignoreChromeError */
|
||||
/* exported contentScripts */
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
Reinject content scripts when the extension is reloaded/updated.
|
||||
Not used in Firefox as it reinjects automatically.
|
||||
*/
|
||||
|
||||
bgReady.all.then(() => {
|
||||
const contentScripts = (() => {
|
||||
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);
|
||||
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);
|
||||
return {injectToTab, injectToAllTabs};
|
||||
|
||||
function injectToTab({url, tabId, frameId = null}) {
|
||||
for (const script of SCRIPTS) {
|
||||
|
@ -49,7 +40,7 @@ bgReady.all.then(() => {
|
|||
const options = {
|
||||
runAt: script.run_at,
|
||||
allFrames: script.all_frames,
|
||||
matchAboutBlank: script.match_about_blank,
|
||||
matchAboutBlank: script.match_about_blank
|
||||
};
|
||||
if (frameId !== null) {
|
||||
options.allFrames = false;
|
||||
|
@ -62,56 +53,16 @@ bgReady.all.then(() => {
|
|||
}
|
||||
|
||||
function injectToAllTabs() {
|
||||
return browser.tabs.query({}).then(tabs => {
|
||||
return queryTabs().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 {
|
||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||
if (tab.width) {
|
||||
injectToTab({
|
||||
url: tab.pendingUrl || tab.url,
|
||||
tabId: tab.id,
|
||||
url: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
319
background/db.js
319
background/db.js
|
@ -1,150 +1,227 @@
|
|||
/* global addAPI */// common.js
|
||||
/* global chromeLocal */// storage-util.js
|
||||
/* global cloneError */// worker-util.js
|
||||
/* global deepCopy */// toolbox.js
|
||||
/* global prefs */
|
||||
/* global chromeLocal ignoreChromeError workerUtil */
|
||||
/* exported db */
|
||||
/*
|
||||
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/
|
||||
*/
|
||||
'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),
|
||||
});
|
||||
let exec;
|
||||
const preparing = prepare();
|
||||
return {
|
||||
styles: getProxy(DB),
|
||||
exec: (...args) =>
|
||||
preparing.then(() => exec(...args))
|
||||
};
|
||||
|
||||
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;
|
||||
function prepare() {
|
||||
return withPromise(shouldUseIndexedDB).then(
|
||||
ok => {
|
||||
if (ok) {
|
||||
useIndexedDB();
|
||||
} else {
|
||||
useChromeStorage();
|
||||
}
|
||||
},
|
||||
err => {
|
||||
useChromeStorage(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function tryUsingIndexedDB() {
|
||||
function shouldUseIndexedDB() {
|
||||
// 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
|
||||
// for reliablility and in localStorage for fast synchronous access
|
||||
// (FF may block localStorage depending on its privacy options)
|
||||
// note that it may throw when accessing the variable
|
||||
// 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();
|
||||
// test localStorage
|
||||
const fallbackSet = localStorage.dbInChromeStorage;
|
||||
if (fallbackSet === 'true') {
|
||||
return false;
|
||||
}
|
||||
chromeLocal.setValue(FALLBACK, false);
|
||||
return dbExecIndexedDB;
|
||||
if (fallbackSet === 'false') {
|
||||
return true;
|
||||
}
|
||||
// test storage.local
|
||||
return chromeLocal.get('dbInChromeStorage')
|
||||
.then(data => {
|
||||
if (data && data.dbInChromeStorage) {
|
||||
return false;
|
||||
}
|
||||
return testDBSize()
|
||||
.then(ok => ok || testDBMutation());
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
function withPromise(fn) {
|
||||
try {
|
||||
return Promise.resolve(fn());
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function useChromeStorage(err) {
|
||||
chromeLocal.setValue(FALLBACK, true);
|
||||
function testDBSize() {
|
||||
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1)
|
||||
.then(event => (
|
||||
event.target.result &&
|
||||
event.target.result.length &&
|
||||
event.target.result[0]
|
||||
));
|
||||
}
|
||||
|
||||
function testDBMutation() {
|
||||
return dbExecIndexedDB('put', {id: -1})
|
||||
.then(() => dbExecIndexedDB('get', -1))
|
||||
.then(event => {
|
||||
if (!event.target.result) {
|
||||
throw new Error('failed to get previously put item');
|
||||
}
|
||||
if (event.target.result.id !== -1) {
|
||||
throw new Error('item id is wrong');
|
||||
}
|
||||
return dbExecIndexedDB('delete', -1);
|
||||
})
|
||||
.then(() => true);
|
||||
}
|
||||
|
||||
function useChromeStorage(err) {
|
||||
exec = dbExecChromeStorage;
|
||||
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
|
||||
if (err) {
|
||||
chromeLocal.setValue(FALLBACK + 'Reason', cloneError(err));
|
||||
chromeLocal.setValue('dbInChromeStorageReason', workerUtil.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);
|
||||
localStorage.dbInChromeStorage = 'true';
|
||||
}
|
||||
|
||||
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 useIndexedDB() {
|
||||
exec = dbExecIndexedDB;
|
||||
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
|
||||
localStorage.dbInChromeStorage = 'false';
|
||||
}
|
||||
|
||||
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));
|
||||
};
|
||||
});
|
||||
function dbExecIndexedDB(method, ...args) {
|
||||
return open().then(database => {
|
||||
if (!method) {
|
||||
return database;
|
||||
}
|
||||
idb.createObjectStore(sn, ID_AS_KEY[dbName] ? {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
} : undefined);
|
||||
if (method === 'putMany') {
|
||||
return putMany(database, ...args);
|
||||
}
|
||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
||||
const transaction = database.transaction(['styles'], mode);
|
||||
const store = transaction.objectStore('styles');
|
||||
return storeRequest(store, method, ...args);
|
||||
});
|
||||
|
||||
function storeRequest(store, method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store[method](...args);
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('stylish', 2);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = reject;
|
||||
request.onupgradeneeded = event => {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function putMany(database, items) {
|
||||
const transaction = database.transaction(['styles'], 'readwrite');
|
||||
const store = transaction.objectStore('styles');
|
||||
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
|
||||
}
|
||||
}
|
||||
|
||||
function dbExecChromeStorage(method, data) {
|
||||
const STYLE_KEY_PREFIX = 'style-';
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||
.then(result => ({target: {result}}));
|
||||
|
||||
case 'put':
|
||||
if (!data.id) {
|
||||
return getMaxId().then(id => {
|
||||
data.id = id + 1;
|
||||
return dbExecChromeStorage('put', data);
|
||||
});
|
||||
}
|
||||
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||
|
||||
case 'putMany': {
|
||||
const newItems = data.filter(i => !i.id);
|
||||
const doPut = () =>
|
||||
chromeLocal.set(data.reduce((o, item) => {
|
||||
o[STYLE_KEY_PREFIX + item.id] = item;
|
||||
return o;
|
||||
}, {}))
|
||||
.then(() => data.map(d => ({target: {result: d.id}})));
|
||||
if (newItems.length) {
|
||||
return getMaxId().then(id => {
|
||||
for (const item of newItems) {
|
||||
item.id = ++id;
|
||||
}
|
||||
return doPut();
|
||||
});
|
||||
}
|
||||
return doPut();
|
||||
}
|
||||
|
||||
case 'delete':
|
||||
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||
|
||||
case 'getAll':
|
||||
return getAllStyles()
|
||||
.then(styles => ({target: {result: styles}}));
|
||||
}
|
||||
return Promise.reject();
|
||||
|
||||
function getAllStyles() {
|
||||
return chromeLocal.get(null).then(storage => {
|
||||
const styles = [];
|
||||
for (const key in storage) {
|
||||
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||
styles.push(storage[key]);
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
});
|
||||
}
|
||||
|
||||
function getMaxId() {
|
||||
return getAllStyles().then(styles => {
|
||||
let result = 0;
|
||||
for (const style of styles) {
|
||||
if (style.id > result) {
|
||||
result = style.id;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
})();
|
91
background/icon-util.js
Normal file
91
background/icon-util.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
/* global ignoreChromeError */
|
||||
/* exported iconUtil */
|
||||
'use strict';
|
||||
|
||||
const iconUtil = (() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
// https://github.com/openstyles/stylus/issues/335
|
||||
let noCanvas;
|
||||
const imageDataCache = new Map();
|
||||
// test if canvas is usable
|
||||
const canvasReady = loadImage('/images/icon/16.png')
|
||||
.then(imageData => {
|
||||
noCanvas = imageData.data.every(b => b === 255);
|
||||
});
|
||||
|
||||
return extendNative({
|
||||
/*
|
||||
Cache imageData for paths
|
||||
*/
|
||||
setIcon,
|
||||
setBadgeText
|
||||
});
|
||||
|
||||
function loadImage(url) {
|
||||
let result = imageDataCache.get(url);
|
||||
if (!result) {
|
||||
result = new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = () => {
|
||||
const w = canvas.width = img.width;
|
||||
const h = canvas.height = img.height;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
resolve(ctx.getImageData(0, 0, w, h));
|
||||
};
|
||||
img.onerror = reject;
|
||||
});
|
||||
imageDataCache.set(url, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setIcon(data) {
|
||||
canvasReady.then(() => {
|
||||
if (noCanvas) {
|
||||
chrome.browserAction.setIcon(data, ignoreChromeError);
|
||||
return;
|
||||
}
|
||||
const pending = [];
|
||||
data.imageData = {};
|
||||
for (const [key, url] of Object.entries(data.path)) {
|
||||
pending.push(loadImage(url)
|
||||
.then(imageData => {
|
||||
data.imageData[key] = imageData;
|
||||
}));
|
||||
}
|
||||
Promise.all(pending).then(() => {
|
||||
delete data.path;
|
||||
chrome.browserAction.setIcon(data, ignoreChromeError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setBadgeText(data) {
|
||||
try {
|
||||
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
||||
chrome.browserAction.setBadgeText(data, ignoreChromeError);
|
||||
} catch (e) {
|
||||
// FIXME: skip pre-rendered tabs?
|
||||
chrome.browserAction.setBadgeText(data);
|
||||
}
|
||||
}
|
||||
|
||||
function extendNative(target) {
|
||||
return new Proxy(target, {
|
||||
get: (target, prop) => {
|
||||
// FIXME: do we really need this?
|
||||
if (!chrome.browserAction ||
|
||||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
|
||||
return () => {};
|
||||
}
|
||||
if (target[prop]) {
|
||||
return target[prop];
|
||||
}
|
||||
return chrome.browserAction[prop].bind(chrome.browserAction);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -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'}],
|
||||
});
|
||||
}
|
||||
});
|
75
background/navigator-util.js
Normal file
75
background/navigator-util.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/* global promisify CHROME URLS */
|
||||
/* exported navigatorUtil */
|
||||
'use strict';
|
||||
|
||||
const navigatorUtil = (() => {
|
||||
const handler = {
|
||||
urlChange: null
|
||||
};
|
||||
const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs));
|
||||
return extendNative({onUrlChange});
|
||||
|
||||
function onUrlChange(fn) {
|
||||
initUrlChange();
|
||||
handler.urlChange.push(fn);
|
||||
}
|
||||
|
||||
function initUrlChange() {
|
||||
if (handler.urlChange) {
|
||||
return;
|
||||
}
|
||||
handler.urlChange = [];
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener(data =>
|
||||
fixNTPUrl(data)
|
||||
.then(() => executeCallbacks(handler.urlChange, data, 'committed'))
|
||||
.catch(console.error)
|
||||
);
|
||||
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||
fixNTPUrl(data)
|
||||
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'))
|
||||
.catch(console.error)
|
||||
);
|
||||
|
||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||
fixNTPUrl(data)
|
||||
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'))
|
||||
.catch(console.error)
|
||||
);
|
||||
}
|
||||
|
||||
function fixNTPUrl(data) {
|
||||
if (
|
||||
!CHROME ||
|
||||
!URLS.chromeProtectsNTP ||
|
||||
!data.url.startsWith('https://www.google.') ||
|
||||
!data.url.includes('/_/chrome/newtab?')
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return tabGet(data.tabId)
|
||||
.then(tab => {
|
||||
if (tab.url === 'chrome://newtab/') {
|
||||
data.url = tab.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeCallbacks(callbacks, data, type) {
|
||||
for (const cb of callbacks) {
|
||||
cb(data, type);
|
||||
}
|
||||
}
|
||||
|
||||
function extendNative(target) {
|
||||
return new Proxy(target, {
|
||||
get: (target, prop) => {
|
||||
if (target[prop]) {
|
||||
return target[prop];
|
||||
}
|
||||
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
102
background/openusercss-api.js
Normal file
102
background/openusercss-api.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
'use strict';
|
||||
|
||||
(() => {
|
||||
// begin:nanographql - Tiny graphQL client library
|
||||
// Author: yoshuawuyts (https://github.com/yoshuawuyts)
|
||||
// License: MIT
|
||||
// Modified by DecentM to fit project standards
|
||||
|
||||
const getOpname = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/;
|
||||
const gql = str => {
|
||||
str = Array.isArray(str) ? str.join('') : str;
|
||||
const name = getOpname.exec(str);
|
||||
|
||||
return variables => {
|
||||
const data = {query: str};
|
||||
if (variables) data.variables = JSON.stringify(variables);
|
||||
if (name && name.length) {
|
||||
const operationName = name[2];
|
||||
if (operationName) data.operationName = name[2];
|
||||
}
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
};
|
||||
|
||||
// end:nanographql
|
||||
|
||||
const api = 'https://api.openusercss.org';
|
||||
const doQuery = ({id}, queryString) => {
|
||||
const query = gql(queryString);
|
||||
|
||||
return fetch(api, {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
body: query({
|
||||
id
|
||||
})
|
||||
})
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||
/**
|
||||
* This function can be used to retrieve a theme object from the
|
||||
* GraphQL API, set above
|
||||
*
|
||||
* Example:
|
||||
* chrome.runtime.sendMessage({
|
||||
* 'method': 'oucThemeById',
|
||||
* 'id': '5a2f819f7c57c751001b49df'
|
||||
* }, console.log);
|
||||
*
|
||||
* @param {ID} $0.id MongoDB style ID
|
||||
* @returns {Promise.<{data: object}>} The GraphQL result with the `theme` object
|
||||
*/
|
||||
|
||||
oucThemeById: params => doQuery(params, `
|
||||
query($id: ID!) {
|
||||
theme(id: $id) {
|
||||
_id
|
||||
title
|
||||
description
|
||||
createdAt
|
||||
lastUpdate
|
||||
version
|
||||
screenshots
|
||||
user {
|
||||
_id
|
||||
displayname
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
|
||||
/**
|
||||
* This function can be used to retrieve a user object from the
|
||||
* GraphQL API, set above
|
||||
*
|
||||
* Example:
|
||||
* chrome.runtime.sendMessage({
|
||||
* 'method': 'oucUserById',
|
||||
* 'id': '5a2f0361ba666f0b00b9c827'
|
||||
* }, console.log);
|
||||
*
|
||||
* @param {ID} $0.id MongoDB style ID
|
||||
* @returns {Promise.<{data: object}>} The GraphQL result with the `user` object
|
||||
*/
|
||||
|
||||
oucUserById: params => doQuery(params, `
|
||||
query($id: ID!) {
|
||||
user(id: $id) {
|
||||
_id
|
||||
displayname
|
||||
avatarUrl
|
||||
smallAvatarUrl
|
||||
bio
|
||||
}
|
||||
}
|
||||
`),
|
||||
});
|
||||
})();
|
104
background/search-db.js
Normal file
104
background/search-db.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/* global API_METHODS styleManager tryRegExp debounce */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
// toLocaleLowerCase cache, autocleared after 1 minute
|
||||
const cache = new Map();
|
||||
// top-level style properties to be searched
|
||||
const PARTS = {
|
||||
name: searchText,
|
||||
url: searchText,
|
||||
sourceCode: searchText,
|
||||
sections: searchSections,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param params
|
||||
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
|
||||
* @param {number[]} [params.ids] - if not specified, all styles are searched
|
||||
* @returns {number[]} - array of matched styles ids
|
||||
*/
|
||||
API_METHODS.searchDB = ({query, ids}) => {
|
||||
let rx, words, icase, matchUrl;
|
||||
query = query.trim();
|
||||
|
||||
if (/^url:/i.test(query)) {
|
||||
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
||||
if (matchUrl) {
|
||||
return styleManager.getStylesByUrl(matchUrl)
|
||||
.then(results => results.map(r => r.data.id));
|
||||
}
|
||||
}
|
||||
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
||||
rx = tryRegExp(RegExp.$1, RegExp.$2);
|
||||
}
|
||||
if (!rx) {
|
||||
words = query
|
||||
.split(/(".*?")|\s+/)
|
||||
.filter(Boolean)
|
||||
.map(w => w.startsWith('"') && w.endsWith('"')
|
||||
? w.slice(1, -1)
|
||||
: w)
|
||||
.filter(w => w.length > 1);
|
||||
words = words.length ? words : [query];
|
||||
icase = words.some(w => w === lower(w));
|
||||
}
|
||||
|
||||
return styleManager.getAllStyles().then(styles => {
|
||||
if (ids) {
|
||||
const idSet = new Set(ids);
|
||||
styles = styles.filter(s => idSet.has(s.id));
|
||||
}
|
||||
const results = [];
|
||||
for (const style of styles) {
|
||||
const id = style.id;
|
||||
if (!query || words && !words.length) {
|
||||
results.push(id);
|
||||
continue;
|
||||
}
|
||||
for (const part in PARTS) {
|
||||
const text = style[part];
|
||||
if (text && PARTS[part](text, rx, words, icase)) {
|
||||
results.push(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cache.size) debounce(clearCache, 60e3);
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
function searchText(text, rx, words, icase) {
|
||||
if (rx) return rx.test(text);
|
||||
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
|
||||
if (words.every(w => text.includes(w))) return true;
|
||||
text = lower(text);
|
||||
}
|
||||
}
|
||||
|
||||
function searchSections(sections, rx, words, icase) {
|
||||
for (const section of sections) {
|
||||
for (const prop in section) {
|
||||
const value = section[prop];
|
||||
if (typeof value === 'string') {
|
||||
if (searchText(value, rx, words, icase)) return true;
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.some(str => searchText(str, rx, words, icase))) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(text) {
|
||||
let result = cache.get(text);
|
||||
if (result) return result;
|
||||
result = text.toLocaleLowerCase();
|
||||
cache.set(text, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
})();
|
File diff suppressed because it is too large
Load Diff
|
@ -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,14 +1,7 @@
|
|||
/* global API */// msg.js
|
||||
/* global addAPI */// common.js
|
||||
/* global isEmptyObj */// toolbox.js
|
||||
/* global prefs */
|
||||
/* global API_METHODS styleManager CHROME prefs updateIconBadge */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Uses chrome.tabs.insertCSS
|
||||
*/
|
||||
|
||||
(() => {
|
||||
API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||
const ACTIONS = {
|
||||
styleApply,
|
||||
styleDeleted,
|
||||
|
@ -18,33 +11,32 @@
|
|||
prefChanged,
|
||||
updateCount,
|
||||
};
|
||||
const NOP = new Error('NOP');
|
||||
const NOP = Promise.resolve(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();
|
||||
},
|
||||
});
|
||||
return function (request) {
|
||||
const action = ACTIONS[request.method];
|
||||
return !action ? NOP :
|
||||
action(request, this.sender)
|
||||
.catch(onError)
|
||||
.then(maybeToggleObserver);
|
||||
};
|
||||
|
||||
function updateCount(request, sender) {
|
||||
const {tab, frameId} = sender;
|
||||
function updateCount(request, {tab, frameId}) {
|
||||
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));
|
||||
updateIconBadge(tab.id, Object.keys(frameStyles).length);
|
||||
}
|
||||
|
||||
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
||||
|
@ -55,8 +47,7 @@
|
|||
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
||||
return NOP;
|
||||
}
|
||||
return API.styles.getSectionsByUrl(url, id).then(sections => {
|
||||
delete sections.cfg;
|
||||
return styleManager.getSectionsByUrl(url, id).then(sections => {
|
||||
const tasks = [];
|
||||
for (const section of Object.values(sections)) {
|
||||
const styleId = section.id;
|
||||
|
@ -133,7 +124,7 @@
|
|||
}
|
||||
const {tab, frameId} = sender;
|
||||
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
||||
if (isEmptyObj(frameStyles)) {
|
||||
if (isEmpty(frameStyles)) {
|
||||
return NOP;
|
||||
}
|
||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
|
||||
|
@ -170,7 +161,7 @@
|
|||
const tabFrames = cache.get(tabId);
|
||||
if (tabFrames && frameId in tabFrames) {
|
||||
delete tabFrames[frameId];
|
||||
if (isEmptyObj(tabFrames)) {
|
||||
if (isEmpty(tabFrames)) {
|
||||
onTabRemoved(tabId);
|
||||
}
|
||||
}
|
||||
|
@ -186,9 +177,9 @@
|
|||
}
|
||||
|
||||
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
|
||||
if (isEmptyObj(frameStyles)) {
|
||||
if (isEmpty(frameStyles)) {
|
||||
delete tabFrames[frameId];
|
||||
if (isEmptyObj(tabFrames)) {
|
||||
if (isEmpty(tabFrames)) {
|
||||
cache.delete(tabId);
|
||||
}
|
||||
return true;
|
||||
|
@ -231,4 +222,11 @@
|
|||
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
|
||||
.catch(onError);
|
||||
}
|
||||
|
||||
function isEmpty(obj) {
|
||||
for (const k in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
})();
|
283
background/update.js
Normal file
283
background/update.js
Normal file
|
@ -0,0 +1,283 @@
|
|||
/* global styleSectionsEqual prefs download tryJSONparse ignoreChromeError
|
||||
calcStyleDigest getStyleWithNoCode debounce chromeLocal
|
||||
usercss semverCompare styleJSONseemsValid
|
||||
API_METHODS styleManager */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
||||
const STATES = {
|
||||
UPDATED: 'updated',
|
||||
SKIPPED: 'skipped',
|
||||
|
||||
// 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 ALARM_NAME = 'scheduledUpdate';
|
||||
const MIN_INTERVAL_MS = 60e3;
|
||||
|
||||
let lastUpdateTime = parseInt(localStorage.lastUpdateTime) || Date.now();
|
||||
let checkingAll = false;
|
||||
let logQueue = [];
|
||||
let logLastWriteTime = 0;
|
||||
|
||||
const retrying = new Set();
|
||||
|
||||
API_METHODS.updateCheckAll = checkAllStyles;
|
||||
API_METHODS.updateCheck = checkStyle;
|
||||
API_METHODS.getUpdaterStates = () => STATES;
|
||||
|
||||
prefs.subscribe(['updateInterval'], schedule);
|
||||
schedule();
|
||||
chrome.alarms.onAlarm.addListener(onAlarm);
|
||||
|
||||
return {checkAllStyles, checkStyle, STATES};
|
||||
|
||||
function checkAllStyles({
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
observe,
|
||||
} = {}) {
|
||||
resetInterval();
|
||||
checkingAll = true;
|
||||
retrying.clear();
|
||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
||||
return styleManager.getAllStyles().then(styles => {
|
||||
styles = styles.filter(style => style.updateUrl);
|
||||
if (port) port.postMessage({count: styles.length});
|
||||
log('');
|
||||
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
||||
return Promise.all(
|
||||
styles.map(style =>
|
||||
checkStyle({style, port, save, ignoreDigest})));
|
||||
}).then(() => {
|
||||
if (port) port.postMessage({done: true});
|
||||
if (port) port.disconnect();
|
||||
log('');
|
||||
checkingAll = false;
|
||||
retrying.clear();
|
||||
});
|
||||
}
|
||||
|
||||
function checkStyle({
|
||||
id,
|
||||
style,
|
||||
port,
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
}) {
|
||||
/*
|
||||
Original style digests are calculated in these cases:
|
||||
* style is installed or updated from server
|
||||
* style is checked for an update and its code is equal to the server code
|
||||
|
||||
Update check proceeds in these cases:
|
||||
* style has the original digest and it's equal to the current digest
|
||||
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
|
||||
* [ignoreDigest: none/false] style doesn't yet have the original digest
|
||||
so we compare the code to the server code and if it's the same we save the digest,
|
||||
otherwise we skip the style and report MAYBE_EDITED status
|
||||
|
||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||
*/
|
||||
return fetchStyle()
|
||||
.then(() => {
|
||||
if (!ignoreDigest) {
|
||||
return calcStyleDigest(style)
|
||||
.then(checkIfEdited);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (style.usercssData) {
|
||||
return maybeUpdateUsercss();
|
||||
}
|
||||
return maybeUpdateUSO();
|
||||
})
|
||||
.then(maybeSave)
|
||||
.then(reportSuccess)
|
||||
.catch(reportFailure);
|
||||
|
||||
function fetchStyle() {
|
||||
if (style) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return styleManager.get(id)
|
||||
.then(style_ => {
|
||||
style = style_;
|
||||
});
|
||||
}
|
||||
|
||||
function reportSuccess(saved) {
|
||||
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
|
||||
const info = {updated: true, style: saved};
|
||||
if (port) port.postMessage(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
function reportFailure(error) {
|
||||
if ((
|
||||
error === 503 || // Service Unavailable
|
||||
error === 429 // Too Many Requests
|
||||
) && !retrying.has(id)) {
|
||||
retrying.add(id);
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(checkStyle({id, style, port, save, ignoreDigest}));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
error = error === 0 ? 'server unreachable' : error;
|
||||
// UserCSS metadata error returns an object; e.g. "Invalid @var color..."
|
||||
if (typeof error === 'object' && error.message) {
|
||||
error = error.message;
|
||||
}
|
||||
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`);
|
||||
const info = {error, STATES, style: getStyleWithNoCode(style)};
|
||||
if (port) port.postMessage(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
function checkIfEdited(digest) {
|
||||
if (style.originalDigest && style.originalDigest !== digest) {
|
||||
return Promise.reject(STATES.EDITED);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeUpdateUSO() {
|
||||
return download(style.md5Url).then(md5 => {
|
||||
if (!md5 || md5.length !== 32) {
|
||||
return Promise.reject(STATES.ERROR_MD5);
|
||||
}
|
||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(STATES.SAME_MD5);
|
||||
}
|
||||
// USO can't handle POST requests for style json
|
||||
return download(style.updateUrl, {body: null})
|
||||
.then(text => {
|
||||
const style = tryJSONparse(text);
|
||||
if (style) {
|
||||
// USO may not provide a correctly updated originalMd5 (#555)
|
||||
style.originalMd5 = md5;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function maybeUpdateUsercss() {
|
||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||
return download(style.updateUrl).then(text =>
|
||||
usercss.buildMeta(text).then(json => {
|
||||
const {usercssData: {version}} = style;
|
||||
const {usercssData: {version: newVersion}} = json;
|
||||
switch (Math.sign(semverCompare(version, newVersion))) {
|
||||
case 0:
|
||||
// re-install is invalid in a soft upgrade
|
||||
if (!ignoreDigest) {
|
||||
const sameCode = text === style.sourceCode;
|
||||
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
// downgrade is always invalid
|
||||
return Promise.reject(STATES.ERROR_VERSION);
|
||||
}
|
||||
return usercss.buildCode(json);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function maybeSave(json = {}) {
|
||||
// usercss is already validated while building
|
||||
if (!json.usercssData && !styleJSONseemsValid(json)) {
|
||||
return Promise.reject(STATES.ERROR_JSON);
|
||||
}
|
||||
|
||||
json.id = style.id;
|
||||
json.updateDate = Date.now();
|
||||
|
||||
// keep current state
|
||||
delete json.enabled;
|
||||
|
||||
// keep local name customizations
|
||||
if (style.originalName !== style.name && style.name !== json.name) {
|
||||
delete json.name;
|
||||
} else {
|
||||
json.originalName = json.name;
|
||||
}
|
||||
|
||||
const newStyle = Object.assign({}, style, json);
|
||||
if (styleSectionsEqual(json, style, {checkSource: true})) {
|
||||
// update digest even if save === false as there might be just a space added etc.
|
||||
return styleManager.installStyle(newStyle)
|
||||
.then(saved => {
|
||||
style.originalDigest = saved.originalDigest;
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
});
|
||||
}
|
||||
|
||||
if (!style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(STATES.MAYBE_EDITED);
|
||||
}
|
||||
|
||||
return save ?
|
||||
API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) :
|
||||
newStyle;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
localStorage.lastUpdateTime = lastUpdateTime = Date.now();
|
||||
schedule();
|
||||
}
|
||||
|
||||
function log(text) {
|
||||
logQueue.push({text, time: new Date().toLocaleString()});
|
||||
debounce(flushQueue, text && checkingAll ? 1000 : 0);
|
||||
}
|
||||
|
||||
function flushQueue(lines) {
|
||||
if (!lines) {
|
||||
chromeLocal.getValue('updateLog', []).then(flushQueue);
|
||||
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 = [];
|
||||
}
|
||||
})();
|
188
background/usercss-helper.js
Normal file
188
background/usercss-helper.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
/* global API_METHODS usercss chromeLocal styleManager FIREFOX deepCopy openURL
|
||||
download */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
API_METHODS.installUsercss = installUsercss;
|
||||
API_METHODS.editSaveUsercss = editSaveUsercss;
|
||||
API_METHODS.configUsercssVars = configUsercssVars;
|
||||
|
||||
API_METHODS.buildUsercss = build;
|
||||
API_METHODS.openUsercssInstallPage = install;
|
||||
|
||||
API_METHODS.findUsercss = find;
|
||||
|
||||
const TEMP_CODE_PREFIX = 'tempUsercssCode';
|
||||
const TEMP_CODE_CLEANUP_DELAY = 60e3;
|
||||
let tempCodeLastWriteDate = 0;
|
||||
if (FIREFOX) {
|
||||
// the temp code is created on direct installation of usercss URLs in FF
|
||||
// and can be left behind in case the install page didn't open in time before
|
||||
// the extension was updated/reloaded/disabled or the browser was closed
|
||||
setTimeout(function poll() {
|
||||
if (Date.now() - tempCodeLastWriteDate < TEMP_CODE_CLEANUP_DELAY) {
|
||||
setTimeout(poll, TEMP_CODE_CLEANUP_DELAY);
|
||||
return;
|
||||
}
|
||||
chrome.storage.local.get(null, storage => {
|
||||
const leftovers = [];
|
||||
for (const key in storage) {
|
||||
if (key.startsWith(TEMP_CODE_PREFIX)) {
|
||||
leftovers.push(key);
|
||||
}
|
||||
}
|
||||
if (leftovers.length) {
|
||||
chrome.storage.local.remove(leftovers);
|
||||
}
|
||||
});
|
||||
}, TEMP_CODE_CLEANUP_DELAY);
|
||||
}
|
||||
|
||||
function buildMeta(style) {
|
||||
if (style.usercssData) {
|
||||
return Promise.resolve(style);
|
||||
}
|
||||
|
||||
// allow sourceCode to be normalized
|
||||
const {sourceCode} = style;
|
||||
delete style.sourceCode;
|
||||
|
||||
return usercss.buildMeta(sourceCode)
|
||||
.then(newStyle => Object.assign(newStyle, style));
|
||||
}
|
||||
|
||||
function assignVars(style) {
|
||||
return find(style)
|
||||
.then(dup => {
|
||||
if (dup) {
|
||||
style.id = dup.id;
|
||||
// preserve style.vars during update
|
||||
return usercss.assignVars(style, dup)
|
||||
.then(() => style);
|
||||
}
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the source, find the duplication, and build sections with variables
|
||||
* @param _
|
||||
* @param {String} _.sourceCode
|
||||
* @param {Boolean=} _.checkDup
|
||||
* @param {Boolean=} _.metaOnly
|
||||
* @param {Object} _.vars
|
||||
* @param {Boolean=} _.assignVars
|
||||
* @returns {Promise<{style, dup:Boolean?}>}
|
||||
*/
|
||||
function build({
|
||||
styleId,
|
||||
sourceCode,
|
||||
checkDup,
|
||||
metaOnly,
|
||||
vars,
|
||||
assignVars = false,
|
||||
}) {
|
||||
return usercss.buildMeta(sourceCode)
|
||||
.then(style => {
|
||||
const findDup = checkDup || assignVars ?
|
||||
find(styleId ? {id: styleId} : style) : Promise.resolve();
|
||||
return Promise.all([
|
||||
metaOnly ? style : doBuild(style, findDup),
|
||||
findDup
|
||||
]);
|
||||
})
|
||||
.then(([style, dup]) => ({style, dup}));
|
||||
|
||||
function doBuild(style, findDup) {
|
||||
if (vars || assignVars) {
|
||||
const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup;
|
||||
return getOld
|
||||
.then(oldStyle => usercss.assignVars(style, oldStyle))
|
||||
.then(() => usercss.buildCode(style));
|
||||
}
|
||||
return usercss.buildCode(style);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the style within aditional properties then inherit variable values
|
||||
// from the old style.
|
||||
function parse(style) {
|
||||
return buildMeta(style)
|
||||
.then(buildMeta)
|
||||
.then(assignVars)
|
||||
.then(usercss.buildCode);
|
||||
}
|
||||
|
||||
// FIXME: simplify this to `installUsercss(sourceCode)`?
|
||||
function installUsercss(style) {
|
||||
return parse(style)
|
||||
.then(styleManager.installStyle);
|
||||
}
|
||||
|
||||
// FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
|
||||
function editSaveUsercss(style) {
|
||||
return parse(style)
|
||||
.then(styleManager.editSave);
|
||||
}
|
||||
|
||||
function configUsercssVars(id, vars) {
|
||||
return styleManager.get(id)
|
||||
.then(style => {
|
||||
const newStyle = deepCopy(style);
|
||||
newStyle.usercssData.vars = vars;
|
||||
return usercss.buildCode(newStyle);
|
||||
})
|
||||
.then(style => styleManager.installStyle(style, 'config'))
|
||||
.then(style => style.usercssData.vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Style|{name:string, namespace:string}} styleOrData
|
||||
* @returns {Style}
|
||||
*/
|
||||
function find(styleOrData) {
|
||||
if (styleOrData.id) {
|
||||
return styleManager.get(styleOrData.id);
|
||||
}
|
||||
const {name, namespace} = styleOrData.usercssData || styleOrData;
|
||||
return styleManager.getAllStyles().then(styleList => {
|
||||
for (const dup of styleList) {
|
||||
const data = dup.usercssData;
|
||||
if (!data) continue;
|
||||
if (data.name === name &&
|
||||
data.namespace === namespace) {
|
||||
return dup;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function install({url, direct, downloaded, tab}, sender = this.sender) {
|
||||
tab = tab !== undefined ? tab : sender.tab;
|
||||
url = url || tab.url;
|
||||
if (direct && !downloaded) {
|
||||
prefetchCodeForInstallation(tab.id, url);
|
||||
}
|
||||
return openURL({
|
||||
url: '/install-usercss.html' +
|
||||
'?updateUrl=' + encodeURIComponent(url) +
|
||||
'&tabId=' + tab.id +
|
||||
(direct ? '&direct=yes' : ''),
|
||||
index: tab.index + 1,
|
||||
openerTabId: tab.id,
|
||||
currentWindow: null,
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchCodeForInstallation(tabId, url) {
|
||||
const key = TEMP_CODE_PREFIX + tabId;
|
||||
tempCodeLastWriteDate = Date.now();
|
||||
Promise.all([
|
||||
download(url),
|
||||
chromeLocal.setValue(key, {loading: true}),
|
||||
]).then(([code]) => {
|
||||
chromeLocal.setValue(key, code);
|
||||
setTimeout(() => chromeLocal.remove(key), TEMP_CODE_CLEANUP_DELAY);
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
606
content/apply.js
606
content/apply.js
|
@ -1,193 +1,258 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global StyleInjector */
|
||||
/* global prefs */
|
||||
/* eslint no-var: 0 */
|
||||
/* global msg API prefs createStyleInjector */
|
||||
/* exported APPLY */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
if (window.INJECTED === 1) return;
|
||||
window.INJECTED = 1;
|
||||
|
||||
/** true -> when the page styles are received,
|
||||
* false -> when disableAll mode is on at start, the styles won't be sent
|
||||
* so while disableAll lasts we can ignore messages about style updates because
|
||||
* the tab will explicitly ask for all styles in bulk when disableAll mode ends */
|
||||
let hasStyles = false;
|
||||
let isDisabled = false;
|
||||
let isTab = !chrome.tabs || location.pathname !== '/popup.html';
|
||||
const order = {main: [], prio: []};
|
||||
const calcOrder = ({id}) =>
|
||||
(order.prio[id] || 0) * 1e6 ||
|
||||
order.main[id] ||
|
||||
id + .5e6; // no order = at the end of `main`
|
||||
const isFrame = window !== parent;
|
||||
const isFrameAboutBlank = isFrame && location.href === 'about:blank';
|
||||
const isUnstylable = !chrome.app && document instanceof XMLDocument;
|
||||
const styleInjector = StyleInjector({
|
||||
compare: (a, b) => calcOrder(a) - calcOrder(b),
|
||||
onUpdate: onInjectorUpdate,
|
||||
// some weird bug in new Chrome: the content script gets injected multiple times
|
||||
// define a constant so it throws when redefined
|
||||
const APPLY = (() => {
|
||||
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
|
||||
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
|
||||
const IS_OWN_PAGE = location.protocol.endsWith('-extension:');
|
||||
const setStyleContent = createSetStyleContent();
|
||||
const styleInjector = createStyleInjector({
|
||||
compare: (a, b) => a.id - b.id,
|
||||
setStyleContent,
|
||||
onUpdate: onInjectorUpdate
|
||||
});
|
||||
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
|
||||
let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
|
||||
location.href;
|
||||
|
||||
// save it now because chrome.runtime will be unavailable in the orphaned script
|
||||
const orphanEventId = chrome.runtime.id;
|
||||
let isOrphaned;
|
||||
// firefox doesn't orphanize content scripts so the old elements stay
|
||||
if (!chrome.app) styleInjector.clearOrphans();
|
||||
|
||||
/** @type chrome.runtime.Port */
|
||||
let port;
|
||||
let lazyBadge = isFrame;
|
||||
let parentDomain;
|
||||
|
||||
/* about:blank iframes are often used by sites for file upload or background tasks
|
||||
* and they may break if unexpected DOM stuff is present at `load` event
|
||||
* so we'll add the styles only if the iframe becomes visible */
|
||||
const xoEventId = `${Math.random()}`;
|
||||
/** @type IntersectionObserver */
|
||||
let xo;
|
||||
window[Symbol.for('xo')] = (el, cb) => {
|
||||
if (!xo) xo = new IntersectionObserver(onIntersect, {rootMargin: '100%'});
|
||||
el.addEventListener(xoEventId, cb, {once: true});
|
||||
xo.observe(el);
|
||||
};
|
||||
|
||||
// FIXME: move this to background page when following bugs are fixed:
|
||||
// https://bugzil.la/1587723, https://crbug.com/968651
|
||||
const mqDark = matchMedia('(prefers-color-scheme: dark)');
|
||||
mqDark.onchange = e => API.colorScheme.updateSystemPreferDark(e.matches);
|
||||
mqDark.onchange(mqDark);
|
||||
|
||||
// Declare all vars before init() or it'll throw due to "temporal dead zone" of const/let
|
||||
init();
|
||||
|
||||
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
|
||||
if (!isTab) {
|
||||
chrome.tabs.getCurrent(tab => {
|
||||
isTab = Boolean(tab);
|
||||
if (tab && styleInjector.list.length) updateCount();
|
||||
});
|
||||
}
|
||||
|
||||
msg.onTab(applyOnMessage);
|
||||
window.addEventListener('pageshow', e => {
|
||||
if (e.isTrusted && e.persisted) { // bfcache
|
||||
updateCount();
|
||||
const docRootObserver = createDocRootObserver({
|
||||
onChange: () => {
|
||||
if (styleInjector.outOfOrder()) {
|
||||
styleInjector.sort();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
const docRewriteObserver = createDocRewriteObserver({
|
||||
onChange: () => {
|
||||
docRootObserver.evade(styleInjector.sort);
|
||||
}
|
||||
});
|
||||
const initializing = init();
|
||||
|
||||
if (!chrome.tabs) {
|
||||
window.dispatchEvent(new CustomEvent(orphanEventId));
|
||||
window.addEventListener(orphanEventId, orphanCheck, true);
|
||||
msg.onTab(applyOnMessage);
|
||||
|
||||
if (!IS_OWN_PAGE) {
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id, {
|
||||
detail: pageObject({method: 'orphan'})
|
||||
}));
|
||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
}
|
||||
|
||||
let parentDomain;
|
||||
|
||||
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
|
||||
if (window !== parent) {
|
||||
prefs.subscribe(['exposeIframes'], updateExposeIframes);
|
||||
}
|
||||
|
||||
function onInjectorUpdate() {
|
||||
if (!isOrphaned) {
|
||||
updateCount();
|
||||
const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
|
||||
onOff('disableAll', updateDisableAll);
|
||||
if (isFrame) {
|
||||
updateExposeIframes();
|
||||
onOff('exposeIframes', updateExposeIframes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (isUnstylable) {
|
||||
await API.styleViaAPI({method: 'styleApply'});
|
||||
if (!IS_OWN_PAGE && styleInjector.list.length) {
|
||||
docRewriteObserver.start();
|
||||
docRootObserver.start();
|
||||
} else {
|
||||
const SYM_ID = 'styles';
|
||||
const SYM = Symbol.for(SYM_ID);
|
||||
const parentStyles = isFrameAboutBlank &&
|
||||
tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
|
||||
const styles =
|
||||
window[SYM] ||
|
||||
parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
|
||||
!isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
|
||||
await API.styles.getSectionsByUrl(matchUrl, null, true);
|
||||
if (styles.cfg) {
|
||||
isDisabled = styles.cfg.disableAll;
|
||||
Object.assign(order, styles.cfg.order);
|
||||
docRewriteObserver.stop();
|
||||
docRootObserver.stop();
|
||||
}
|
||||
updateCount();
|
||||
updateExposeIframes();
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (STYLE_VIA_API) {
|
||||
return API.styleViaAPI({method: 'styleApply'});
|
||||
}
|
||||
return API.getSectionsByUrl(getMatchUrl())
|
||||
.then(result =>
|
||||
applyStyles(result)
|
||||
.then(() => {
|
||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||
// the browsers, especially Firefox, may apply all transitions on page load
|
||||
if (styleInjector.list.some(s => s.code.includes('transition'))) {
|
||||
applyTransitionPatch();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function pageObject(target) {
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts
|
||||
const obj = new window.Object();
|
||||
Object.assign(obj, target);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function createSetStyleContent() {
|
||||
// FF59+ bug workaround
|
||||
// See https://github.com/openstyles/stylus/issues/461
|
||||
// Since it's easy to spoof the browser version in pre-Quantum FF we're checking
|
||||
// for getPreventDefault which got removed in FF59 https://bugzil.la/691151
|
||||
const EVENT_NAME = chrome.runtime.id;
|
||||
let ready;
|
||||
return (el, content, disabled) =>
|
||||
checkPageScript().then(ok => {
|
||||
if (!ok) {
|
||||
el.textContent = content;
|
||||
// https://github.com/openstyles/stylus/issues/693
|
||||
el.disabled = disabled;
|
||||
} else {
|
||||
const detail = pageObject({
|
||||
method: 'setStyleContent',
|
||||
id: el.id,
|
||||
content,
|
||||
disabled
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail}));
|
||||
}
|
||||
});
|
||||
|
||||
function checkPageScript() {
|
||||
if (!ready) {
|
||||
ready = CHROME || IS_OWN_PAGE || Event.prototype.getPreventDefault ?
|
||||
Promise.resolve(false) : injectPageScript();
|
||||
}
|
||||
hasStyles = !isDisabled;
|
||||
if (hasStyles) {
|
||||
window[SYM] = styles;
|
||||
await styleInjector.apply(styles);
|
||||
} else {
|
||||
delete window[SYM];
|
||||
prefs.subscribe('disableAll', updateDisableAll);
|
||||
return ready;
|
||||
}
|
||||
|
||||
function injectPageScript() {
|
||||
const scriptContent = EVENT_NAME => {
|
||||
document.currentScript.remove();
|
||||
const available = checkStyleApplied();
|
||||
if (available) {
|
||||
window.addEventListener(EVENT_NAME, function handler(e) {
|
||||
const {method, id, content, disabled} = e.detail;
|
||||
if (method === 'setStyleContent') {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.textContent = content;
|
||||
el.disabled = disabled;
|
||||
} else if (method === 'orphan') {
|
||||
window.removeEventListener(EVENT_NAME, handler);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail: {
|
||||
method: 'init',
|
||||
available
|
||||
}}));
|
||||
|
||||
function checkStyleApplied() {
|
||||
const style = document.createElement('style');
|
||||
document.documentElement.appendChild(style);
|
||||
const applied = Boolean(style.sheet);
|
||||
style.remove();
|
||||
return applied;
|
||||
}
|
||||
};
|
||||
const code = `(${scriptContent})(${JSON.stringify(EVENT_NAME)})`;
|
||||
// make sure it works in XML
|
||||
const script = document.createElementNS('http://www.w3.org/1999/xhtml', 'script');
|
||||
const {resolve, promise} = deferred();
|
||||
// use inline script because using src is too slow
|
||||
// https://github.com/openstyles/stylus/pull/766
|
||||
script.text = code;
|
||||
script.onerror = resolveFalse;
|
||||
window.addEventListener('error', resolveFalse);
|
||||
window.addEventListener(EVENT_NAME, handleInit);
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
// injection failed if handleInit is not called.
|
||||
resolveFalse();
|
||||
return promise.then(result => {
|
||||
script.remove();
|
||||
window.removeEventListener(EVENT_NAME, handleInit);
|
||||
window.removeEventListener('error', resolveFalse);
|
||||
return result;
|
||||
});
|
||||
|
||||
function resolveFalse() {
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
function handleInit(e) {
|
||||
if (e.detail.method === 'init') {
|
||||
resolve(e.detail.available);
|
||||
}
|
||||
}
|
||||
styleInjector.toggle(hasStyles);
|
||||
}
|
||||
}
|
||||
|
||||
/** Must be executed inside try/catch */
|
||||
function getStylesViaXhr() {
|
||||
const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
|
||||
const url = 'blob:' + chrome.runtime.getURL(blobId);
|
||||
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false); // synchronous
|
||||
xhr.send();
|
||||
URL.revokeObjectURL(url);
|
||||
return JSON.parse(xhr.response);
|
||||
function deferred() {
|
||||
const o = {};
|
||||
o.promise = new Promise((resolve, reject) => {
|
||||
o.resolve = resolve;
|
||||
o.reject = reject;
|
||||
});
|
||||
return o;
|
||||
}
|
||||
|
||||
function getMatchUrl() {
|
||||
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) {}
|
||||
}
|
||||
return matchUrl;
|
||||
}
|
||||
|
||||
function applyOnMessage(request) {
|
||||
const {method} = request;
|
||||
if (isUnstylable) {
|
||||
if (method === 'urlChanged') {
|
||||
if (request.method === 'ping') {
|
||||
return true;
|
||||
}
|
||||
if (STYLE_VIA_API) {
|
||||
if (request.method === 'urlChanged') {
|
||||
request.method = 'styleReplaceAll';
|
||||
}
|
||||
if (/^(style|updateCount)/.test(method)) {
|
||||
API.styleViaAPI(request);
|
||||
return;
|
||||
}
|
||||
API.styleViaAPI(request);
|
||||
return;
|
||||
}
|
||||
|
||||
const {style} = request;
|
||||
switch (method) {
|
||||
case 'ping':
|
||||
return true;
|
||||
|
||||
switch (request.method) {
|
||||
case 'styleDeleted':
|
||||
styleInjector.remove(style.id);
|
||||
styleInjector.remove(request.style.id);
|
||||
break;
|
||||
|
||||
case 'styleUpdated':
|
||||
if (!hasStyles && isDisabled) break;
|
||||
if (style.enabled) {
|
||||
API.styles.getSectionsByUrl(matchUrl, style.id).then(sections =>
|
||||
sections[style.id]
|
||||
? styleInjector.apply(sections)
|
||||
: styleInjector.remove(style.id));
|
||||
if (request.style.enabled) {
|
||||
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
||||
.then(sections => {
|
||||
if (!sections[request.style.id]) {
|
||||
styleInjector.remove(request.style.id);
|
||||
} else {
|
||||
applyStyles(sections);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
styleInjector.remove(style.id);
|
||||
styleInjector.remove(request.style.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleAdded':
|
||||
if (!hasStyles && isDisabled) break;
|
||||
if (style.enabled) {
|
||||
API.styles.getSectionsByUrl(matchUrl, style.id)
|
||||
.then(styleInjector.apply);
|
||||
if (request.style.enabled) {
|
||||
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
||||
.then(applyStyles);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleSort':
|
||||
Object.assign(order, request.order);
|
||||
styleInjector.sort();
|
||||
case 'urlChanged':
|
||||
API.getSectionsByUrl(getMatchUrl())
|
||||
.then(replaceAll);
|
||||
break;
|
||||
|
||||
case 'urlChanged':
|
||||
if (!hasStyles && isDisabled || matchUrl === request.url) break;
|
||||
matchUrl = request.url;
|
||||
API.styles.getSectionsByUrl(matchUrl).then(sections => {
|
||||
hasStyles = true;
|
||||
styleInjector.replace(sections);
|
||||
});
|
||||
case 'backgroundReady':
|
||||
initializing
|
||||
.catch(err => {
|
||||
if (msg.RX_NO_RECEIVER.test(err.message)) {
|
||||
return init();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
break;
|
||||
|
||||
case 'updateCount':
|
||||
|
@ -196,76 +261,205 @@
|
|||
}
|
||||
}
|
||||
|
||||
function updateDisableAll(key, disableAll) {
|
||||
isDisabled = disableAll;
|
||||
if (isUnstylable) {
|
||||
function doDisableAll(disableAll) {
|
||||
if (STYLE_VIA_API) {
|
||||
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
||||
} else if (!hasStyles && !disableAll) {
|
||||
init();
|
||||
} else {
|
||||
styleInjector.toggle(!disableAll);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
|
||||
const attr = 'stylus-iframe';
|
||||
const el = document.documentElement;
|
||||
if (!el) return; // got no styles so styleInjector didn't wait for <html>
|
||||
if (!value || !styleInjector.list.length) {
|
||||
el.removeAttribute(attr);
|
||||
function fetchParentDomain() {
|
||||
if (parentDomain) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return API.getTabUrlPrefix()
|
||||
.then(newDomain => {
|
||||
parentDomain = newDomain;
|
||||
});
|
||||
}
|
||||
|
||||
function updateExposeIframes() {
|
||||
if (!prefs.get('exposeIframes') || window === parent || !styleInjector.list.length) {
|
||||
document.documentElement.removeAttribute('stylus-iframe');
|
||||
} else {
|
||||
if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
|
||||
// Check first to avoid triggering DOM mutation
|
||||
if (el.getAttribute(attr) !== parentDomain) {
|
||||
el.setAttribute(attr, parentDomain);
|
||||
}
|
||||
fetchParentDomain().then(() => {
|
||||
document.documentElement.setAttribute('stylus-iframe', parentDomain);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
if (!isTab) return;
|
||||
if (isFrame) {
|
||||
if (!port && styleInjector.list.length) {
|
||||
port = chrome.runtime.connect({name: 'iframe'});
|
||||
} else if (port && !styleInjector.list.length) {
|
||||
port.disconnect();
|
||||
}
|
||||
if (lazyBadge && performance.now() > 1000) lazyBadge = false;
|
||||
if (window !== parent) {
|
||||
// we don't care about iframes
|
||||
return;
|
||||
}
|
||||
(isUnstylable ?
|
||||
API.styleViaAPI({method: 'updateCount'}) :
|
||||
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
|
||||
).catch(msg.ignoreError);
|
||||
}
|
||||
|
||||
function onFrameElementInView(cb) {
|
||||
parent[parent.Symbol.for('xo')](frameElement, cb);
|
||||
}
|
||||
|
||||
/** @param {IntersectionObserverEntry[]} entries */
|
||||
function onIntersect(entries) {
|
||||
for (const e of entries) {
|
||||
if (e.isIntersecting) {
|
||||
xo.unobserve(e.target);
|
||||
e.target.dispatchEvent(new Event(xoEventId));
|
||||
}
|
||||
if (/^\w+?-extension:\/\/.+(popup|options)\.html$/.test(location.href)) {
|
||||
// popup and the option page are not tabs
|
||||
return;
|
||||
}
|
||||
if (STYLE_VIA_API) {
|
||||
API.styleViaAPI({method: 'updateCount'}).catch(msg.ignoreError);
|
||||
return;
|
||||
}
|
||||
// we have to send the tabId so we can't use `sendBg` that is used by `API`
|
||||
msg.send({
|
||||
method: 'invokeAPI',
|
||||
name: 'updateIconBadge',
|
||||
args: [styleInjector.list.length]
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
function tryCatch(func, ...args) {
|
||||
function rootReady() {
|
||||
if (document.documentElement) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
new MutationObserver((mutations, observer) => {
|
||||
if (document.documentElement) {
|
||||
observer.disconnect();
|
||||
resolve();
|
||||
}
|
||||
}).observe(document, {childList: true});
|
||||
});
|
||||
}
|
||||
|
||||
function applyStyles(sections) {
|
||||
const styles = Object.values(sections);
|
||||
if (!styles.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return rootReady().then(() =>
|
||||
docRootObserver.evade(() =>
|
||||
styleInjector.addMany(
|
||||
styles.map(s => ({id: s.id, code: s.code.join('')}))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function replaceAll(newStyles) {
|
||||
styleInjector.replaceAll(
|
||||
Object.values(newStyles)
|
||||
.map(s => ({id: s.id, code: s.code.join('')}))
|
||||
);
|
||||
}
|
||||
|
||||
function applyTransitionPatch() {
|
||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||
// the browsers, especially Firefox, may apply all transitions on page load
|
||||
const el = styleInjector.createStyle('transition-patch');
|
||||
// FIXME: this will trigger docRootObserver and cause a resort. We should
|
||||
// move this function into style-injector.
|
||||
document.documentElement.appendChild(el);
|
||||
setStyleContent(el, `
|
||||
:root:not(#\\0):not(#\\0) * {
|
||||
transition: none !important;
|
||||
}
|
||||
`)
|
||||
.then(afterPaint)
|
||||
.then(() => {
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function afterPaint() {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function orphanCheck(e) {
|
||||
if (e && e.detail.method !== 'orphan') {
|
||||
return;
|
||||
}
|
||||
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
styleInjector.clear();
|
||||
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
try {
|
||||
return func(...args);
|
||||
msg.off(applyOnMessage);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
if (chrome.runtime.id) return;
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
window.removeEventListener(orphanEventId, orphanCheck, true);
|
||||
mqDark.onchange = null;
|
||||
isOrphaned = true;
|
||||
setTimeout(styleInjector.clear, 1000); // avoiding FOUC
|
||||
tryCatch(msg.off, applyOnMessage);
|
||||
function createDocRewriteObserver({onChange}) {
|
||||
// detect documentElement being rewritten from inside the script
|
||||
let root;
|
||||
let observing = false;
|
||||
let timer;
|
||||
const observer = new MutationObserver(check);
|
||||
return {start, stop};
|
||||
|
||||
function start() {
|
||||
if (observing) return;
|
||||
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
||||
root = document.documentElement;
|
||||
timer = setTimeout(check);
|
||||
observer.observe(document, {childList: true});
|
||||
observing = true;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!observing) return;
|
||||
clearTimeout(timer);
|
||||
observer.disconnect();
|
||||
observing = false;
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (root !== document.documentElement) {
|
||||
root = document.documentElement;
|
||||
onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createDocRootObserver({onChange}) {
|
||||
let digest = 0;
|
||||
let lastCalledTime = NaN;
|
||||
let observing = false;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (digest) {
|
||||
if (performance.now() - lastCalledTime > 1000) {
|
||||
digest = 0;
|
||||
} else if (digest > 5) {
|
||||
throw new Error('The page keeps generating mutations. Skip the event.');
|
||||
}
|
||||
}
|
||||
if (onChange()) {
|
||||
digest++;
|
||||
lastCalledTime = performance.now();
|
||||
}
|
||||
});
|
||||
return {start, stop, evade};
|
||||
|
||||
function start() {
|
||||
if (observing) return;
|
||||
observer.observe(document.documentElement, {childList: true});
|
||||
observing = true;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!observing) return;
|
||||
// FIXME: do we need this?
|
||||
observer.takeRecords();
|
||||
observer.disconnect();
|
||||
observing = false;
|
||||
}
|
||||
|
||||
function evade(fn) {
|
||||
if (!observing) {
|
||||
return fn();
|
||||
}
|
||||
stop();
|
||||
const r = fn();
|
||||
start();
|
||||
return r;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/* global API */// msg.js
|
||||
'use strict';
|
||||
|
||||
// onCommitted may fire twice
|
||||
// Note, we're checking against a literal `1`, not just `if (truthy)`,
|
||||
// because <html id="INJECTED"> is exposed per HTML spec as a global variable and `window.INJECTED`.
|
||||
|
||||
if (window.INJECTED_GREASYFORK !== 1) {
|
||||
window.INJECTED_GREASYFORK = 1;
|
||||
addEventListener('message', async function onMessage(e) {
|
||||
if (e.origin === location.origin &&
|
||||
e.data &&
|
||||
e.data.name &&
|
||||
e.data.type === 'style-version-query') {
|
||||
removeEventListener('message', onMessage);
|
||||
const style = await API.usercss.find(e.data) || {};
|
||||
const {version} = style.usercssData || {};
|
||||
postMessage({type: 'style-version', version}, '*');
|
||||
}
|
||||
});
|
||||
}
|
174
content/install-hook-openusercss.js
Normal file
174
content/install-hook-openusercss.js
Normal file
|
@ -0,0 +1,174 @@
|
|||
/* global API */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const allowedOrigins = [
|
||||
'https://openusercss.org',
|
||||
'https://openusercss.com'
|
||||
];
|
||||
|
||||
const sendPostMessage = message => {
|
||||
if (allowedOrigins.includes(location.origin)) {
|
||||
window.postMessage(message, location.origin);
|
||||
}
|
||||
};
|
||||
|
||||
const askHandshake = () => {
|
||||
// Tell the page that we exist and that it should send the handshake
|
||||
sendPostMessage({
|
||||
type: 'ouc-begin-handshake'
|
||||
});
|
||||
};
|
||||
|
||||
// Listen for queries by the site and respond with a callback object
|
||||
const sendInstalledCallback = styleData => {
|
||||
sendPostMessage({
|
||||
type: 'ouc-is-installed-response',
|
||||
style: styleData
|
||||
});
|
||||
};
|
||||
|
||||
const installedHandler = event => {
|
||||
if (event.data
|
||||
&& event.data.type === 'ouc-is-installed'
|
||||
&& allowedOrigins.includes(event.origin)
|
||||
) {
|
||||
API.findUsercss({
|
||||
name: event.data.name,
|
||||
namespace: event.data.namespace
|
||||
}).then(style => {
|
||||
const data = {event};
|
||||
const callbackObject = {
|
||||
installed: Boolean(style),
|
||||
enabled: style.enabled,
|
||||
name: data.name,
|
||||
namespace: data.namespace
|
||||
};
|
||||
|
||||
sendInstalledCallback(callbackObject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const attachInstalledListeners = () => {
|
||||
window.addEventListener('message', installedHandler);
|
||||
};
|
||||
|
||||
const doHandshake = () => {
|
||||
// This is a representation of features that Stylus is capable of
|
||||
const implementedFeatures = [
|
||||
'install-usercss',
|
||||
'event:install-usercss',
|
||||
'event:is-installed',
|
||||
'configure-after-install',
|
||||
'builtin-editor',
|
||||
'create-usercss',
|
||||
'edit-usercss',
|
||||
'import-moz-export',
|
||||
'export-moz-export',
|
||||
'update-manual',
|
||||
'update-auto',
|
||||
'export-json-backups',
|
||||
'import-json-backups',
|
||||
'manage-local'
|
||||
];
|
||||
const reportedFeatures = [];
|
||||
|
||||
// The handshake question includes a list of required and optional features
|
||||
// we match them with features we have implemented, and build a union array.
|
||||
event.data.featuresList.required.forEach(feature => {
|
||||
if (implementedFeatures.includes(feature)) {
|
||||
reportedFeatures.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
event.data.featuresList.optional.forEach(feature => {
|
||||
if (implementedFeatures.includes(feature)) {
|
||||
reportedFeatures.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
// We send the handshake response, which includes the key we got, plus some
|
||||
// additional metadata
|
||||
sendPostMessage({
|
||||
type: 'ouc-handshake-response',
|
||||
key: event.data.key,
|
||||
extension: {
|
||||
name: manifest.name,
|
||||
capabilities: reportedFeatures
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handshakeHandler = event => {
|
||||
if (event.data
|
||||
&& event.data.type === 'ouc-handshake-question'
|
||||
&& allowedOrigins.includes(event.origin)
|
||||
) {
|
||||
doHandshake();
|
||||
}
|
||||
};
|
||||
|
||||
const attachHandshakeListeners = () => {
|
||||
// Wait for the handshake request, then start it
|
||||
window.addEventListener('message', handshakeHandler);
|
||||
};
|
||||
|
||||
const sendInstallCallback = data => {
|
||||
// Send an install callback to the site in order to let it know
|
||||
// we were able to install the theme and it may display a success message
|
||||
sendPostMessage({
|
||||
type: 'ouc-install-callback',
|
||||
key: data.key
|
||||
});
|
||||
};
|
||||
|
||||
const installHandler = event => {
|
||||
if (event.data
|
||||
&& event.data.type === 'ouc-install-usercss'
|
||||
&& allowedOrigins.includes(event.origin)
|
||||
) {
|
||||
API.installUsercss({
|
||||
name: event.data.title,
|
||||
sourceCode: event.data.code,
|
||||
}).then(style => {
|
||||
sendInstallCallback({
|
||||
enabled: style.enabled,
|
||||
key: event.data.key
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const attachInstallListeners = () => {
|
||||
// Wait for an install event, then save the theme
|
||||
window.addEventListener('message', installHandler);
|
||||
};
|
||||
|
||||
const orphanCheck = () => {
|
||||
const eventName = chrome.runtime.id + '-install-hook-openusercss';
|
||||
const orphanCheckRequest = () => {
|
||||
// If we can't get the UI language, it means we are orphaned, and should
|
||||
// remove our event handlers
|
||||
if (chrome.i18n && chrome.i18n.getUILanguage()) return true;
|
||||
|
||||
window.removeEventListener('message', installHandler);
|
||||
window.removeEventListener('message', handshakeHandler);
|
||||
window.removeEventListener('message', installedHandler);
|
||||
window.removeEventListener(eventName, orphanCheckRequest, true);
|
||||
};
|
||||
|
||||
// Send the event before we listen for it, for other possible
|
||||
// running instances of the content script.
|
||||
dispatchEvent(new Event(eventName));
|
||||
addEventListener(eventName, orphanCheckRequest, true);
|
||||
};
|
||||
|
||||
orphanCheck();
|
||||
|
||||
attachHandshakeListeners();
|
||||
attachInstallListeners();
|
||||
attachInstalledListeners();
|
||||
askHandshake();
|
||||
})();
|
|
@ -1,26 +1,123 @@
|
|||
/* global API */
|
||||
'use strict';
|
||||
|
||||
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
|
||||
if (typeof window.oldCode !== 'string') {
|
||||
window.oldCode = (document.querySelector('body > pre') || document.body).textContent;
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (port.name !== 'downloadSelf') return;
|
||||
port.onMessage.addListener(async ({id, force}) => {
|
||||
const msg = {id};
|
||||
try {
|
||||
const code = await (await fetch(location.href, {mode: 'same-origin'})).text();
|
||||
if (code !== window.oldCode || force) {
|
||||
msg.code = window.oldCode = code;
|
||||
}
|
||||
} catch (error) {
|
||||
msg.error = error.message || `${error}`;
|
||||
}
|
||||
port.postMessage(msg);
|
||||
});
|
||||
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
|
||||
addEventListener('pagehide', () => port.disconnect(), {once: true});
|
||||
});
|
||||
}
|
||||
(() => {
|
||||
// some weird bug in new Chrome: the content script gets injected multiple times
|
||||
if (typeof window.initUsercssInstall === 'function') return;
|
||||
if (!/text\/(css|plain)/.test(document.contentType) ||
|
||||
!/==userstyle==/i.test(document.body.textContent)) {
|
||||
return;
|
||||
}
|
||||
window.initUsercssInstall = () => {};
|
||||
|
||||
// passing the result to tabs.executeScript
|
||||
window.oldCode; // eslint-disable-line no-unused-expressions
|
||||
orphanCheck();
|
||||
|
||||
const DELAY = 500;
|
||||
const url = location.href;
|
||||
let sourceCode, port, timer;
|
||||
|
||||
chrome.runtime.onConnect.addListener(onConnected);
|
||||
API.openUsercssInstallPage({url})
|
||||
.catch(err => alert(err));
|
||||
|
||||
function onConnected(newPort) {
|
||||
port = newPort;
|
||||
port.onDisconnect.addListener(stop);
|
||||
port.onMessage.addListener(onMessage);
|
||||
}
|
||||
|
||||
function onMessage(msg, port) {
|
||||
switch (msg.method) {
|
||||
case 'getSourceCode':
|
||||
fetchText(url)
|
||||
.then(text => {
|
||||
sourceCode = sourceCode || text;
|
||||
port.postMessage({
|
||||
method: msg.method + 'Response',
|
||||
sourceCode,
|
||||
});
|
||||
})
|
||||
.catch(err => port.postMessage({
|
||||
method: msg.method + 'Response',
|
||||
error: err.message || String(err),
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'liveReloadStart':
|
||||
start();
|
||||
break;
|
||||
|
||||
case 'liveReloadStop':
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchText(url) {
|
||||
// XHR throws in Chrome 49
|
||||
// FIXME: choose a correct version
|
||||
// https://github.com/openstyles/stylus/issues/560
|
||||
if (getChromeVersion() <= 49) {
|
||||
return fetch(url)
|
||||
.then(r => r.text())
|
||||
.catch(() => fetchTextXHR(url));
|
||||
}
|
||||
return fetchTextXHR(url);
|
||||
}
|
||||
|
||||
function fetchTextXHR(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// you can't use fetch in Chrome under 'file:' protocol
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url);
|
||||
xhr.addEventListener('load', () => resolve(xhr.responseText));
|
||||
xhr.addEventListener('error', () => reject(xhr));
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
function getChromeVersion() {
|
||||
const match = navigator.userAgent.match(/chrome\/(\d+)/i);
|
||||
return match ? Number(match[1]) : undefined;
|
||||
}
|
||||
|
||||
function start() {
|
||||
timer = timer || setTimeout(check, DELAY);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
function check() {
|
||||
fetchText(url)
|
||||
.then(text => {
|
||||
if (sourceCode === text) return;
|
||||
sourceCode = text;
|
||||
port.postMessage({method: 'sourceCodeChanged', sourceCode});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(chrome.i18n.getMessage('liveReloadError', error));
|
||||
})
|
||||
.then(() => {
|
||||
timer = null;
|
||||
start();
|
||||
});
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
const eventName = chrome.runtime.id + '-install-hook-usercss';
|
||||
const orphanCheckRequest = () => {
|
||||
if (chrome.i18n && chrome.i18n.getUILanguage()) return true;
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
removeEventListener(eventName, orphanCheckRequest, true);
|
||||
try {
|
||||
chrome.runtime.onConnect.removeListener(onConnected);
|
||||
} catch (e) {}
|
||||
};
|
||||
dispatchEvent(new Event(eventName));
|
||||
addEventListener(eventName, orphanCheckRequest, true);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,324 +1,497 @@
|
|||
/* global API */// msg.js
|
||||
/* global cloneInto msg API */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
|
||||
if (window.INJECTED_USO === 1) return;
|
||||
window.INJECTED_USO = 1;
|
||||
(() => {
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
||||
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
|
||||
const usoId = RegExp.$1;
|
||||
const USO = 'https://userstyles.org';
|
||||
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
|
||||
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
|
||||
const CLICK = [
|
||||
['#install_stylish_style_button', onInstall],
|
||||
['#update_stylish_style_button', onInstall],
|
||||
['.customize_style_button', onCustomize],
|
||||
['.uninstall_stylish_style_button', onUninstall],
|
||||
];
|
||||
const pageEventId = `${performance.now()}${Math.random()}`;
|
||||
const contentEventId = pageEventId + ':';
|
||||
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
|
||||
const $ = (sel, base = document) => base.querySelector(sel);
|
||||
const toggleListener = (isOn, ...args) => (isOn ? addEventListener : removeEventListener)(...args);
|
||||
const togglePageListener = isOn => toggleListener(isOn, contentEventId, onPageEvent, true);
|
||||
document.addEventListener('stylishInstallChrome', onClick);
|
||||
document.addEventListener('stylishUpdateChrome', onClick);
|
||||
|
||||
const mo = new MutationObserver(onMutation);
|
||||
const observeColors = isOn =>
|
||||
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
|
||||
: mo.disconnect();
|
||||
msg.on(onMessage);
|
||||
|
||||
let style, dup, md5, pageData, badKeys;
|
||||
onDOMready().then(() => {
|
||||
window.postMessage({
|
||||
direction: 'from-content-script',
|
||||
message: 'StylishInstalled',
|
||||
}, '*');
|
||||
});
|
||||
|
||||
runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
|
||||
addEventListener(orphanEventId, orphanCheck, true);
|
||||
addEventListener('click', onClick, true);
|
||||
togglePageListener(true);
|
||||
let gotBody = false;
|
||||
let currentMd5;
|
||||
new MutationObserver(observeDOM).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
observeDOM();
|
||||
|
||||
[md5, dup] = await Promise.all([
|
||||
fetch(md5Url).then(r => r.text()),
|
||||
API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
|
||||
.then(sendVarsToPage),
|
||||
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
|
||||
]);
|
||||
|
||||
if (!dup) {
|
||||
sendStylishEvent('styleCanBeInstalledChrome');
|
||||
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
|
||||
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
|
||||
sendStylishEvent('styleCanBeUpdatedChrome');
|
||||
} else {
|
||||
sendStylishEvent('styleAlreadyInstalledChrome');
|
||||
}
|
||||
|
||||
async function onClick(e) {
|
||||
for (const [sel, fn] of CLICK) {
|
||||
const el = e.target.closest(sel);
|
||||
if (!el) continue;
|
||||
try {
|
||||
el.disabled = true;
|
||||
await fn(e);
|
||||
} catch (e) {
|
||||
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
|
||||
} finally {
|
||||
el.disabled = false;
|
||||
}
|
||||
function observeDOM() {
|
||||
if (!gotBody) {
|
||||
if (!document.body) return;
|
||||
gotBody = true;
|
||||
// TODO: remove the following statement when USO pagination title is fixed
|
||||
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
||||
const md5Url = getMeta('stylish-md5-url') || location.href;
|
||||
Promise.all([
|
||||
API.findStyle({md5Url}),
|
||||
getResource(md5Url)
|
||||
])
|
||||
.then(checkUpdatability);
|
||||
}
|
||||
if (document.getElementById('install_button')) {
|
||||
onDOMready().then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
sendEvent(sendEvent.lastEvent);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onCustomize() {
|
||||
const ss = $('#style-settings');
|
||||
const willShow = !ss || !ss.offsetHeight;
|
||||
observeColors(willShow);
|
||||
toggleListener(willShow, 'change', onChange);
|
||||
}
|
||||
|
||||
async function onInstall(e) {
|
||||
const {id} = dup;
|
||||
e.stopPropagation();
|
||||
if (!style) await buildStyle();
|
||||
style = dup = await API.usercss.install(style, {
|
||||
dup: {id},
|
||||
vars: getPageVars(),
|
||||
});
|
||||
sendStylishEvent('styleInstalledChrome');
|
||||
API.uso.pingback(id);
|
||||
}
|
||||
|
||||
function onUninstall() {
|
||||
const {id} = dup;
|
||||
dup = style = false;
|
||||
observeColors(false);
|
||||
removeEventListener('change', onChange);
|
||||
return API.styles.delete(id);
|
||||
}
|
||||
|
||||
function onChange({target: el}) {
|
||||
if (dup && el.matches('[name^="ik-"], [type=file]')) {
|
||||
API.usercss.configVars(dup.id, getPageVars());
|
||||
function onMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'ping':
|
||||
// orphaned content script check
|
||||
return true;
|
||||
case 'openSettings':
|
||||
openSettings();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function onMutation(mutations) {
|
||||
for (const {target: el} of mutations) {
|
||||
if (el.style.display === 'none' &&
|
||||
/^ik-/.test(el.name) &&
|
||||
/^#[\da-f]{6}$/.test(el.value)) {
|
||||
onChange({target: el});
|
||||
}
|
||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||
we need to fix this URL using "stylish-update-url" meta key
|
||||
*/
|
||||
function getStyleURL() {
|
||||
const textUrl = getMeta('stylish-update-url') || '';
|
||||
const jsonUrl = getMeta('stylish-code-chrome') ||
|
||||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
||||
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
||||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||
}
|
||||
|
||||
function checkUpdatability([installedStyle, md5]) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
}));
|
||||
currentMd5 = md5;
|
||||
if (!installedStyle) {
|
||||
sendEvent({type: 'styleCanBeInstalledChrome'});
|
||||
return;
|
||||
}
|
||||
const isCustomizable = /\?/.test(installedStyle.updateUrl);
|
||||
const md5Url = getMeta('stylish-md5-url');
|
||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||
reportUpdatable(isCustomizable || md5 !== installedStyle.originalMd5);
|
||||
} else {
|
||||
getStyleJson().then(json => {
|
||||
reportUpdatable(
|
||||
isCustomizable ||
|
||||
!json ||
|
||||
!styleSectionsEqual(json, installedStyle));
|
||||
});
|
||||
}
|
||||
|
||||
function prepareInstallButton() {
|
||||
return new Promise(resolve => {
|
||||
const observer = new MutationObserver(check);
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
check();
|
||||
|
||||
function check() {
|
||||
if (document.querySelector('#install_style_button')) {
|
||||
resolve();
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpdatable(isUpdatable) {
|
||||
prepareInstallButton().then(() => {
|
||||
sendEvent({
|
||||
type: isUpdatable
|
||||
? 'styleCanBeUpdatedChrome'
|
||||
: 'styleAlreadyInstalledChrome',
|
||||
detail: {
|
||||
updateUrl: installedStyle.updateUrl
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onPageEvent(e) {
|
||||
pageData = e.detail;
|
||||
togglePageListener(false);
|
||||
}
|
||||
|
||||
async function buildStyle() {
|
||||
if (!pageData) pageData = await (await fetch(apiUrl)).json();
|
||||
({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
|
||||
Object.assign(style, {
|
||||
md5Url,
|
||||
id: dup.id,
|
||||
originalMd5: md5,
|
||||
updateUrl: apiUrl,
|
||||
function sendEvent(event) {
|
||||
sendEvent.lastEvent = event;
|
||||
let {type, detail = null} = event;
|
||||
if (typeof cloneInto !== 'undefined') {
|
||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||
detail = cloneInto({detail}, document);
|
||||
} else {
|
||||
detail = {detail};
|
||||
}
|
||||
onDOMready().then(() => {
|
||||
document.dispatchEvent(new CustomEvent(type, detail));
|
||||
});
|
||||
}
|
||||
|
||||
function getPageVars() {
|
||||
const {vars} = (style || dup).usercssData;
|
||||
for (const el of document.querySelectorAll('[name^="ik-"]')) {
|
||||
const name = el.name.slice(3); // dropping "ik-"
|
||||
const ik = (badKeys || {})[name] || name;
|
||||
const v = vars[ik] || false;
|
||||
const isImage = el.type === 'radio';
|
||||
if (v && (!isImage || el.checked)) {
|
||||
const val = el.value;
|
||||
const isFile = val === 'user-upload';
|
||||
if (isImage && (isFile || val === 'user-url')) {
|
||||
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
|
||||
const ikCust = `${ik}-custom`;
|
||||
v.value = `${ikCust}-dropdown`;
|
||||
vars[ikCust].value = isFile ? getFileUriFromPage(el2) : el2.value;
|
||||
} else {
|
||||
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
|
||||
|
||||
function onClick(event) {
|
||||
if (onClick.processing || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
onClick.processing = true;
|
||||
doInstall()
|
||||
.then(() => {
|
||||
if (!event.type.includes('Update')) {
|
||||
// FIXME: sometimes the button is broken i.e. the button sends
|
||||
// 'install' instead of 'update' event while the style is already
|
||||
// install.
|
||||
// This triggers an incorrect install count but we don't really care.
|
||||
return getResource(getMeta('stylish-install-ping-url-chrome'));
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.then(done);
|
||||
function done() {
|
||||
setTimeout(() => {
|
||||
onClick.processing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function doInstall() {
|
||||
let oldStyle;
|
||||
return API.findStyle({
|
||||
md5Url: getMeta('stylish-md5-url') || location.href
|
||||
}, true)
|
||||
.then(_oldStyle => {
|
||||
oldStyle = _oldStyle;
|
||||
return oldStyle ?
|
||||
oldStyle.name :
|
||||
getResource(getMeta('stylish-description'));
|
||||
})
|
||||
.then(name => {
|
||||
const props = {};
|
||||
if (oldStyle) {
|
||||
props.id = oldStyle.id;
|
||||
}
|
||||
return saveStyleCode(oldStyle ? 'styleUpdate' : 'styleInstall', name, props);
|
||||
});
|
||||
}
|
||||
|
||||
function saveStyleCode(message, name, addProps = {}) {
|
||||
const isNew = message === 'styleInstall';
|
||||
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||
return Promise.reject();
|
||||
}
|
||||
saveStyleCode.confirmed = true;
|
||||
enableUpdateButton(false);
|
||||
return getStyleJson().then(json => {
|
||||
if (!json) {
|
||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||
'https://github.com/openstyles/stylus/issues/195');
|
||||
return;
|
||||
}
|
||||
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
|
||||
return API.installStyle(Object.assign(json, addProps, {originalMd5: currentMd5}))
|
||||
.then(style => {
|
||||
if (!isNew && style.updateUrl.includes('?')) {
|
||||
enableUpdateButton(true);
|
||||
} else {
|
||||
sendEvent({type: 'styleInstalledChrome'});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function enableUpdateButton(state) {
|
||||
const important = s => s.replace(/;/g, '!important;');
|
||||
const button = document.getElementById('update_style_button');
|
||||
if (button) {
|
||||
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
|
||||
const icon = button.querySelector('img[src*=".svg"]');
|
||||
if (icon) {
|
||||
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
|
||||
if (state) {
|
||||
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function getFileUriFromPage(el) {
|
||||
togglePageListener(true);
|
||||
sendPageEvent(el);
|
||||
return pageData;
|
||||
|
||||
function getMeta(name) {
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
function runInPage(fn, ...args) {
|
||||
const div = document.createElement('div');
|
||||
div.attachShadow({mode: 'closed'})
|
||||
.appendChild(document.createElement('script'))
|
||||
.textContent = `(${fn})(${JSON.stringify(args).slice(1, -1)})`;
|
||||
document.documentElement.appendChild(div).remove();
|
||||
}
|
||||
|
||||
function sendPageEvent(data) {
|
||||
dispatchEvent(data instanceof Node
|
||||
? new MouseEvent(pageEventId, {relatedTarget: data})
|
||||
: new CustomEvent(pageEventId, {detail: data}));
|
||||
//* global cloneInto */// WARNING! Firefox requires cloning of an object `detail`
|
||||
}
|
||||
|
||||
function sendStylishEvent(type) {
|
||||
document.dispatchEvent(new Event(type));
|
||||
}
|
||||
|
||||
function sendVarsToPage(style) {
|
||||
if (style) {
|
||||
const vars = (style.usercssData || {}).vars || `${style.updateUrl}`.split('?')[1];
|
||||
if (vars) sendPageEvent('vars:' + JSON.stringify(vars));
|
||||
function getResource(url, options) {
|
||||
if (url.startsWith('#')) {
|
||||
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
|
||||
}
|
||||
return style || false;
|
||||
return API.download(Object.assign({
|
||||
url,
|
||||
timeout: 60e3,
|
||||
// USO can't handle POST requests for style json
|
||||
body: null,
|
||||
}, options))
|
||||
.catch(error => {
|
||||
alert('Error' + (error ? '\n' + error : ''));
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
||||
// instead of "https://update.userstyles.org/#####.md5"
|
||||
function tryFixMd5(style) {
|
||||
if (style && style.md5Url && style.md5Url.includes('update.update')) {
|
||||
style.md5Url = style.md5Url.replace('update.update', 'update');
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
function getStyleJson() {
|
||||
return getResource(getStyleURL(), {responseType: 'json'})
|
||||
.then(style => {
|
||||
if (!style || !Array.isArray(style.sections) || style.sections.length) {
|
||||
return style;
|
||||
}
|
||||
const codeElement = document.getElementById('stylish-code');
|
||||
if (codeElement && !codeElement.textContent.trim()) {
|
||||
return style;
|
||||
}
|
||||
return getResource(getMeta('stylish-update-url'))
|
||||
.then(code => API.parseCss({code}))
|
||||
.then(result => {
|
||||
style.sections = result.sections;
|
||||
return style;
|
||||
});
|
||||
})
|
||||
.then(tryFixMd5)
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
if (!a || !b) {
|
||||
return undefined;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
// order of sections should be identical to account for the case of multiple
|
||||
// sections matching the same URL because the order of rules is part of cascading
|
||||
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
||||
|
||||
function propertiesEqual(secA, secB) {
|
||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||
}
|
||||
|
||||
function equalOrEmpty(a, b, telltale, comparator) {
|
||||
const typeA = a && typeof a[telltale] === 'function';
|
||||
const typeB = b && typeof b[telltale] === 'function';
|
||||
return (
|
||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||
(b === null || b === undefined || (typeB && !b.length))
|
||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||
}
|
||||
|
||||
function arrayMirrors(array1, array2) {
|
||||
return (
|
||||
array1.every(el => array2.includes(el)) &&
|
||||
array2.every(el => array1.includes(el))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState !== 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function openSettings(countdown = 10e3) {
|
||||
const button = document.querySelector('.customize_button');
|
||||
if (button) {
|
||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
setTimeout(function pollArea(countdown = 2000) {
|
||||
const area = document.getElementById('advancedsettings_area');
|
||||
if (area || countdown < 0) {
|
||||
(area || button).scrollIntoView({behavior: 'smooth', block: area ? 'end' : 'center'});
|
||||
} else {
|
||||
setTimeout(pollArea, 100, countdown - 100);
|
||||
}
|
||||
}, 500);
|
||||
} else if (countdown > 0) {
|
||||
setTimeout(openSettings, 100, countdown - 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function orphanCheck() {
|
||||
if (chrome.runtime.id) return true;
|
||||
removeEventListener(orphanEventId, orphanCheck, true);
|
||||
removeEventListener('click', onClick, true);
|
||||
removeEventListener('change', onChange);
|
||||
sendPageEvent('quit');
|
||||
observeColors(false);
|
||||
togglePageListener(false);
|
||||
// TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls
|
||||
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
document.removeEventListener('stylishInstallChrome', onClick);
|
||||
document.removeEventListener('stylishUpdateChrome', onClick);
|
||||
try {
|
||||
msg.off(onMessage);
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
|
||||
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
|
||||
let done, orphaned, vars;
|
||||
// `chrome` may be empty if no extensions use externally_connectable but USO needs it
|
||||
if (!window.chrome) window.chrome = {runtime: {sendMessage: () => {}}};
|
||||
const EXT_ID = 'fjnbnpbmkenffdnngjfgmeleoegfcffe';
|
||||
const {defineProperty} = Object;
|
||||
const {dispatchEvent, CustomEvent, removeEventListener} = window;
|
||||
const apply = Map.call.bind(Map.apply);
|
||||
const OVR = [
|
||||
[chrome.runtime, 'sendMessage', (fn, me, args) => {
|
||||
const [id, /*msg*/, opts, cb = opts] = args;
|
||||
if (id !== EXT_ID) return apply(fn, me, args);
|
||||
if (typeof cb !== 'function') return Promise.resolve(true);
|
||||
cb(true);
|
||||
}],
|
||||
[Response.prototype, 'json', async (fn, me, args) => {
|
||||
const res = await apply(fn, me, args);
|
||||
try {
|
||||
if (!done && me.url === apiUrl) {
|
||||
done = true;
|
||||
send(res);
|
||||
setVars(res);
|
||||
}
|
||||
} catch (e) {}
|
||||
return res;
|
||||
}],
|
||||
[window, 'fetch', (fn, me, args) =>
|
||||
args[0] === `chrome-extension://${EXT_ID}/index.html`
|
||||
? Promise.resolve(new Response('<!doctype html><html lang="en"></html>'))
|
||||
: apply(fn, me, args),
|
||||
],
|
||||
];
|
||||
OVR.forEach(([obj, name, caller], i) => {
|
||||
const orig = obj[name];
|
||||
const ovr = new Proxy(orig, {
|
||||
apply(fn, me, args) {
|
||||
if (orphaned) restore(obj, name, ovr, fn);
|
||||
return (orphaned ? apply : caller)(fn, me, args);
|
||||
},
|
||||
// run in page context
|
||||
document.documentElement.appendChild(document.createElement('script')).text = '(' + (
|
||||
() => {
|
||||
document.currentScript.remove();
|
||||
|
||||
// spoof Stylish extension presence in Chrome
|
||||
if (window.chrome && chrome.app) {
|
||||
const realImage = window.Image;
|
||||
window.Image = function Image(...args) {
|
||||
return new Proxy(new realImage(...args), {
|
||||
get(obj, key) {
|
||||
return obj[key];
|
||||
},
|
||||
set(obj, key, value) {
|
||||
if (key === 'src' && /^chrome-extension:/i.test(value)) {
|
||||
setTimeout(() => typeof obj.onload === 'function' && obj.onload());
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// USO bug workaround: use the actual style settings in API response
|
||||
let settings;
|
||||
const originalResponseJson = Response.prototype.json;
|
||||
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, ''));
|
||||
if (!settings) {
|
||||
Response.prototype.json = originalResponseJson;
|
||||
}
|
||||
});
|
||||
defineProperty(obj, name, {value: ovr});
|
||||
OVR[i] = [obj, name, ovr, orig]; // same args as restore()
|
||||
});
|
||||
/* We set `isInstalled` at page start intentionally not trying to replicate Stylish login events.
|
||||
* This difference allows USO site to detect presence of Stylus (or another similar extension). */
|
||||
window.isInstalled = true;
|
||||
addEventListener(eventId, onCommand, true);
|
||||
function onCommand(e) {
|
||||
if (e.detail === 'quit') {
|
||||
removeEventListener(eventId, onCommand, true);
|
||||
OVR.forEach(restore);
|
||||
done = orphaned = true;
|
||||
} else if (/^vars:/.test(e.detail)) {
|
||||
vars = JSON.parse(e.detail.slice(5));
|
||||
} else if (e.relatedTarget) {
|
||||
send(e.relatedTarget.uploadedData);
|
||||
}
|
||||
}
|
||||
function restore(obj, name, ovr, orig) { // same order as OVR after patching
|
||||
if (obj[name] === ovr) {
|
||||
defineProperty(obj, name, {value: orig});
|
||||
}
|
||||
}
|
||||
function send(data) {
|
||||
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
|
||||
}
|
||||
function setVars(json) {
|
||||
const images = new Map();
|
||||
const isNew = typeof vars === 'object';
|
||||
const badKeys = {};
|
||||
const newKeys = [];
|
||||
const makeKey = ({install_key: key}) => {
|
||||
let res = isNew ? badKeys[key] : key;
|
||||
if (!res) {
|
||||
res = key.replace(/[^-\w]/g, '-');
|
||||
res += newKeys.includes(res) ? '-' : '';
|
||||
if (key !== res) {
|
||||
badKeys[key] = res;
|
||||
newKeys.push(res);
|
||||
Response.prototype.json = function (...args) {
|
||||
return originalResponseJson.call(this, ...args).then(json => {
|
||||
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
if (!isNew) vars = new URLSearchParams(vars);
|
||||
for (const ss of json.style_settings || []) {
|
||||
const ik = makeKey(ss);
|
||||
let value = isNew ? (vars[ik] || {}).value : vars.get('ik-' + ik);
|
||||
if (value == null || !(ss.style_setting_options || [])[0]) {
|
||||
continue;
|
||||
}
|
||||
if (ss.setting_type === 'image') {
|
||||
let isListed;
|
||||
for (const opt of ss.style_setting_options) {
|
||||
isListed |= opt.default = (opt.install_key === value);
|
||||
}
|
||||
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
|
||||
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const def = ss.style_setting_options.find(item => item.default);
|
||||
if (!def || makeKey(def) !== value) {
|
||||
if (def) def.default = false;
|
||||
for (const item of ss.style_setting_options) {
|
||||
if (makeKey(item) === value) {
|
||||
item.default = true;
|
||||
break;
|
||||
Response.prototype.json = originalResponseJson;
|
||||
const images = new Map();
|
||||
for (const jsonSetting of json.style_settings) {
|
||||
let value = settings.get('ik-' + jsonSetting.install_key);
|
||||
if (!value
|
||||
|| !jsonSetting.style_setting_options
|
||||
|| !jsonSetting.style_setting_options[0]) {
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('ik-')) {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
||||
if (!defaultItem || defaultItem.install_key !== value) {
|
||||
if (defaultItem) {
|
||||
defaultItem.default = false;
|
||||
}
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.install_key === value) {
|
||||
item.default = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (jsonSetting.setting_type === 'image') {
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.default) {
|
||||
item.default = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
images.set(jsonSetting.install_key, value);
|
||||
} else {
|
||||
const item = jsonSetting.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key === 'placeholder') {
|
||||
item.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const item = ss.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key === 'placeholder') {
|
||||
item.value = value;
|
||||
if (images.size) {
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('style-settings')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
for (const [name, url] of images.entries()) {
|
||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
if (elUrl) {
|
||||
elUrl.value = url;
|
||||
}
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!images.size) return;
|
||||
return json;
|
||||
});
|
||||
};
|
||||
}
|
||||
) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`;
|
||||
|
||||
// TODO: remove the following statement when USO pagination is fixed
|
||||
if (location.search.includes('category=')) {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('style-settings')) return;
|
||||
if (!document.getElementById('pagination')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
for (const [name, {url, isListed}] of images) {
|
||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
if (elUrl) {
|
||||
elRadio.checked = !isListed;
|
||||
elUrl.value = url;
|
||||
}
|
||||
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
||||
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
links[i].href += category;
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) {
|
||||
new MutationObserver((_, observer) => {
|
||||
const cssButton = document.getElementsByClassName('css_button');
|
||||
if (cssButton.length) {
|
||||
// Click on the "Show CSS Code" button to workaround the JS error
|
||||
cssButton[0].click();
|
||||
cssButton[0].click();
|
||||
observer.disconnect();
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/* global API */// msg.js
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
const ORIGIN = 'https://userstyles.world';
|
||||
const HANDLERS = Object.assign(Object.create(null), {
|
||||
|
||||
async 'usw-ready'() {
|
||||
send({type: 'usw-remove-stylus-button'});
|
||||
if (location.pathname === '/api/oauth/style/new') {
|
||||
const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
|
||||
const data = await API.data.pop('usw' + styleId);
|
||||
send({type: 'usw-fill-new-style', data});
|
||||
}
|
||||
},
|
||||
|
||||
async 'usw-style-info-request'(data) {
|
||||
switch (data.requestType) {
|
||||
case 'installed': {
|
||||
const updateUrl = `${ORIGIN}/api/style/${data.styleID}.user.css`;
|
||||
const style = await API.styles.find({updateUrl});
|
||||
send({
|
||||
type: 'usw-style-info-response',
|
||||
data: {installed: Boolean(style), requestType: 'installed'},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
window.addEventListener('message', ({data, source, origin}) => {
|
||||
// Some browsers don't reveal `source` to extensions e.g. Firefox
|
||||
if (data && (source ? source === window : origin === ORIGIN)) {
|
||||
const fn = HANDLERS[data.type];
|
||||
if (fn) fn(data);
|
||||
}
|
||||
});
|
||||
|
||||
function send(msg) {
|
||||
window.postMessage(msg, ORIGIN);
|
||||
}
|
||||
})();
|
|
@ -1,201 +1,92 @@
|
|||
/* exported createStyleInjector */
|
||||
'use strict';
|
||||
|
||||
/** @type {function(opts):StyleInjector} */
|
||||
window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
|
||||
compare,
|
||||
onUpdate = () => {},
|
||||
}) => {
|
||||
function createStyleInjector({compare, setStyleContent, onUpdate}) {
|
||||
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
|
||||
const PREFIX = 'stylus-';
|
||||
const PATCH_ID = 'transition-patch';
|
||||
// styles are out of order if any of these elements is injected between them
|
||||
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
|
||||
const docRewriteObserver = RewriteObserver(sort);
|
||||
const docRootObserver = RootObserver(sortIfNeeded);
|
||||
const toSafeChar = c => String.fromCharCode(0xFF00 + c.charCodeAt(0) - 0x20);
|
||||
const list = [];
|
||||
const table = new Map();
|
||||
let isEnabled = true;
|
||||
let isTransitionPatched = chrome.app && CSS.supports('accent-color', 'red'); // Chrome 93
|
||||
let exposeStyleName;
|
||||
// will store the original method refs because the page can override them
|
||||
let creationDoc, createElement, createElementNS;
|
||||
let enabled = true;
|
||||
return {
|
||||
// manipulation
|
||||
add,
|
||||
addMany,
|
||||
remove,
|
||||
update,
|
||||
clear,
|
||||
replaceAll,
|
||||
|
||||
return /** @namespace StyleInjector */ {
|
||||
// method
|
||||
toggle,
|
||||
sort,
|
||||
|
||||
// state
|
||||
outOfOrder,
|
||||
list,
|
||||
|
||||
async apply(styleMap) {
|
||||
const styles = styleMapToArray(styleMap);
|
||||
const value = !styles.length
|
||||
? []
|
||||
: await docRootObserver.evade(() => {
|
||||
if (!isTransitionPatched && isEnabled) {
|
||||
applyTransitionPatch(styles);
|
||||
}
|
||||
return styles.map(addUpdate);
|
||||
});
|
||||
emitUpdate();
|
||||
return value;
|
||||
},
|
||||
|
||||
clear() {
|
||||
addRemoveElements(false);
|
||||
list.length = 0;
|
||||
table.clear();
|
||||
emitUpdate();
|
||||
},
|
||||
|
||||
clearOrphans() {
|
||||
for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) {
|
||||
const id = el.id.slice(PREFIX.length);
|
||||
if (/^\d+$/.test(id) || id === PATCH_ID) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
remove(id) {
|
||||
remove(id);
|
||||
emitUpdate();
|
||||
},
|
||||
|
||||
replace(styleMap) {
|
||||
const styles = styleMapToArray(styleMap);
|
||||
const added = new Set(styles.map(s => s.id));
|
||||
const removed = [];
|
||||
for (const style of list) {
|
||||
if (!added.has(style.id)) {
|
||||
removed.push(style.id);
|
||||
}
|
||||
}
|
||||
styles.forEach(addUpdate);
|
||||
removed.forEach(remove);
|
||||
emitUpdate();
|
||||
},
|
||||
|
||||
toggle(enable) {
|
||||
if (isEnabled === enable) return;
|
||||
isEnabled = enable;
|
||||
if (!enable) toggleObservers(false);
|
||||
addRemoveElements(enable);
|
||||
if (enable) toggleObservers(true);
|
||||
},
|
||||
|
||||
sort: sort,
|
||||
// static util
|
||||
createStyle
|
||||
};
|
||||
|
||||
function outOfOrder() {
|
||||
if (!list.length) {
|
||||
return false;
|
||||
}
|
||||
let el = list[0].el;
|
||||
if (el.parentNode !== document.documentElement) {
|
||||
return true;
|
||||
}
|
||||
let i = 0;
|
||||
while (el) {
|
||||
if (i < list.length && el === list[i].el) {
|
||||
i++;
|
||||
} else if (ORDERED_TAGS.has(el.localName)) {
|
||||
return true;
|
||||
}
|
||||
el = el.nextSibling;
|
||||
}
|
||||
// some styles are not injected to the document
|
||||
return i < list.length;
|
||||
}
|
||||
|
||||
function addMany(styles) {
|
||||
const pending = Promise.all(styles.map(_add));
|
||||
emitUpdate();
|
||||
return pending;
|
||||
}
|
||||
|
||||
function add(style) {
|
||||
const el = style.el = createStyle(style);
|
||||
const i = list.findIndex(item => compare(item, style) > 0);
|
||||
const pending = _add(style);
|
||||
emitUpdate();
|
||||
return pending;
|
||||
}
|
||||
|
||||
function _add(style) {
|
||||
if (table.has(style.id)) {
|
||||
return update(style);
|
||||
}
|
||||
style.el = createStyle(style.id);
|
||||
const pending = setStyleContent(style.el, style.code, !enabled);
|
||||
table.set(style.id, style);
|
||||
if (isEnabled) {
|
||||
document.documentElement.insertBefore(el, i < 0 ? null : list[i].el);
|
||||
}
|
||||
list.splice(i < 0 ? list.length : i, 0, style);
|
||||
return el;
|
||||
}
|
||||
|
||||
function addRemoveElements(add) {
|
||||
for (const {el} of list) {
|
||||
if (add) {
|
||||
document.documentElement.appendChild(el);
|
||||
} else {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addUpdate(style) {
|
||||
return table.has(style.id) ? update(style) : add(style);
|
||||
}
|
||||
|
||||
function applyTransitionPatch(styles) {
|
||||
isTransitionPatched = true;
|
||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||
// the browsers, especially Firefox, may apply all transitions on page load
|
||||
if (document.readyState === 'complete' ||
|
||||
document.visibilityState === 'hidden' ||
|
||||
!styles.some(s => s.code.includes('transition'))) {
|
||||
return;
|
||||
}
|
||||
const el = createStyle({id: PATCH_ID, code: `
|
||||
:root:not(#\\0):not(#\\0) * {
|
||||
transition: none !important;
|
||||
}
|
||||
`});
|
||||
document.documentElement.appendChild(el);
|
||||
// wait for the next paint to complete
|
||||
// note: requestAnimationFrame won't fire in inactive tabs
|
||||
requestAnimationFrame(() => setTimeout(() => el.remove()));
|
||||
}
|
||||
|
||||
function createStyle(style = {}) {
|
||||
const {id} = style;
|
||||
if (!creationDoc) initCreationDoc();
|
||||
let el;
|
||||
if (document.documentElement instanceof SVGSVGElement) {
|
||||
// SVG document style
|
||||
el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style');
|
||||
} else if (document instanceof XMLDocument) {
|
||||
// XML document style
|
||||
el = createElementNS.call(creationDoc, 'http://www.w3.org/1999/xhtml', 'style');
|
||||
const nextIndex = list.findIndex(i => compare(i, style) > 0);
|
||||
if (nextIndex < 0) {
|
||||
document.documentElement.appendChild(style.el);
|
||||
list.push(style);
|
||||
} else {
|
||||
// HTML document style; also works on HTML-embedded SVG
|
||||
el = createElement.call(creationDoc, 'style');
|
||||
document.documentElement.insertBefore(style.el, list[nextIndex].el);
|
||||
list.splice(nextIndex, 0, style);
|
||||
}
|
||||
if (id) {
|
||||
el.id = `${PREFIX}${id}`;
|
||||
const oldEl = document.getElementById(el.id);
|
||||
if (oldEl) oldEl.id += '-superseded-by-Stylus';
|
||||
}
|
||||
el.type = 'text/css';
|
||||
// SVG className is not a string, but an instance of SVGAnimatedString
|
||||
el.classList.add('stylus');
|
||||
setTextAndName(el, style);
|
||||
return el;
|
||||
}
|
||||
|
||||
function setTextAndName(el, {id, code = '', name}) {
|
||||
if (exposeStyleName && name) {
|
||||
el.dataset.name = name;
|
||||
name = encodeURIComponent(name.replace(/[?#/']/g, toSafeChar));
|
||||
code += `\n/*# sourceURL=${chrome.runtime.getURL(name)}.user.css#${id} */`;
|
||||
}
|
||||
el.textContent = code;
|
||||
}
|
||||
|
||||
function toggleObservers(shouldStart) {
|
||||
const onOff = shouldStart && isEnabled ? 'start' : 'stop';
|
||||
docRewriteObserver[onOff]();
|
||||
docRootObserver[onOff]();
|
||||
}
|
||||
|
||||
function emitUpdate() {
|
||||
toggleObservers(list.length);
|
||||
onUpdate();
|
||||
}
|
||||
|
||||
/*
|
||||
FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461
|
||||
First we're trying the page context document where inline styles may be forbidden by CSP
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1579345#c3
|
||||
and since userAgent.navigator can be spoofed via about:config or devtools,
|
||||
we're checking for getPreventDefault that was removed in FF59
|
||||
*/
|
||||
function initCreationDoc() {
|
||||
creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
|
||||
if (creationDoc) {
|
||||
({createElement, createElementNS} = creationDoc);
|
||||
const el = document.documentElement.appendChild(createStyle());
|
||||
const isApplied = el.sheet;
|
||||
el.remove();
|
||||
if (isApplied) return;
|
||||
}
|
||||
creationDoc = document;
|
||||
({createElement, createElementNS} = document);
|
||||
return pending;
|
||||
}
|
||||
|
||||
function remove(id) {
|
||||
_remove(id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function _remove(id) {
|
||||
const style = table.get(id);
|
||||
if (!style) return;
|
||||
table.delete(id);
|
||||
|
@ -203,143 +94,91 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
|
|||
style.el.remove();
|
||||
}
|
||||
|
||||
function sort() {
|
||||
docRootObserver.evade(() => {
|
||||
list.sort(compare);
|
||||
addRemoveElements(true);
|
||||
});
|
||||
}
|
||||
|
||||
function sortIfNeeded() {
|
||||
let needsSort;
|
||||
let el = list.length && list[0].el;
|
||||
if (!el) {
|
||||
needsSort = false;
|
||||
} else if (el.parentNode !== creationDoc.documentElement) {
|
||||
needsSort = true;
|
||||
} else {
|
||||
let i = 0;
|
||||
while (el) {
|
||||
if (i < list.length && el === list[i].el) {
|
||||
i++;
|
||||
} else if (ORDERED_TAGS.has(el.localName)) {
|
||||
needsSort = true;
|
||||
break;
|
||||
}
|
||||
el = el.nextElementSibling;
|
||||
}
|
||||
// some styles are not injected to the document
|
||||
if (i < list.length) needsSort = true;
|
||||
}
|
||||
if (needsSort) sort();
|
||||
return needsSort;
|
||||
}
|
||||
|
||||
function styleMapToArray(styleMap) {
|
||||
if (styleMap.cfg) {
|
||||
({exposeStyleName} = styleMap.cfg);
|
||||
delete styleMap.cfg;
|
||||
}
|
||||
return Object.values(styleMap).map(({id, code, name}) => ({
|
||||
id,
|
||||
name,
|
||||
code: code.join(''),
|
||||
}));
|
||||
}
|
||||
|
||||
function update(newStyle) {
|
||||
const {id, code} = newStyle;
|
||||
function update({id, code}) {
|
||||
const style = table.get(id);
|
||||
if (style.code !== code ||
|
||||
style.name !== newStyle.name && exposeStyleName) {
|
||||
style.code = code;
|
||||
setTextAndName(style.el, newStyle);
|
||||
if (style.code === code) return;
|
||||
style.code = code;
|
||||
// workaround for Chrome devtools bug fixed in v65
|
||||
// https://github.com/openstyles/stylus/commit/0fa391732ba8e35fa68f326a560fc04c04b8608b
|
||||
let oldEl;
|
||||
if (CHROME < 3321) {
|
||||
oldEl = style.el;
|
||||
oldEl.id = '';
|
||||
style.el = createStyle(id);
|
||||
oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling);
|
||||
style.el.disabled = !enabled;
|
||||
}
|
||||
return setStyleContent(style.el, code, !enabled)
|
||||
.then(() => oldEl && oldEl.remove());
|
||||
}
|
||||
|
||||
function createStyle(id) {
|
||||
let el;
|
||||
if (document.documentElement instanceof SVGSVGElement) {
|
||||
// SVG document style
|
||||
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||
} else if (document instanceof XMLDocument) {
|
||||
// XML document style
|
||||
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
|
||||
} else {
|
||||
// HTML document style; also works on HTML-embedded SVG
|
||||
el = document.createElement('style');
|
||||
}
|
||||
el.id = `${PREFIX}${id}`;
|
||||
el.type = 'text/css';
|
||||
// SVG className is not a string, but an instance of SVGAnimatedString
|
||||
el.classList.add('stylus');
|
||||
return el;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
for (const style of list) {
|
||||
style.el.remove();
|
||||
}
|
||||
list.length = 0;
|
||||
table.clear();
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function toggle(_enabled) {
|
||||
if (enabled === _enabled) return;
|
||||
enabled = _enabled;
|
||||
for (const style of list) {
|
||||
style.el.disabled = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
function RewriteObserver(onChange) {
|
||||
// detect documentElement being rewritten from inside the script
|
||||
let root;
|
||||
let observing = false;
|
||||
let timer;
|
||||
const observer = new MutationObserver(check);
|
||||
return {start, stop};
|
||||
|
||||
function start() {
|
||||
if (observing) return;
|
||||
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
||||
root = document.documentElement;
|
||||
timer = setTimeout(check);
|
||||
observer.observe(document, {childList: true});
|
||||
observing = true;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!observing) return;
|
||||
clearTimeout(timer);
|
||||
observer.disconnect();
|
||||
observing = false;
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (root !== document.documentElement) {
|
||||
root = document.documentElement;
|
||||
onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function RootObserver(onChange) {
|
||||
let digest = 0;
|
||||
let lastCalledTime = NaN;
|
||||
let observing = false;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (digest) {
|
||||
if (performance.now() - lastCalledTime > 1000) {
|
||||
digest = 0;
|
||||
} else if (digest > 5) {
|
||||
throw new Error('The page keeps generating mutations. Skip the event.');
|
||||
}
|
||||
}
|
||||
if (onChange()) {
|
||||
digest++;
|
||||
lastCalledTime = performance.now();
|
||||
}
|
||||
});
|
||||
return {evade, start, stop};
|
||||
|
||||
function evade(fn) {
|
||||
const restore = observing && start;
|
||||
stop();
|
||||
return new Promise(resolve => run(fn, resolve, waitForRoot))
|
||||
.then(restore);
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (observing) return;
|
||||
observer.observe(document.documentElement, {childList: true});
|
||||
observing = true;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!observing) return;
|
||||
function sort() {
|
||||
list.sort(compare);
|
||||
for (const style of list) {
|
||||
// FIXME: do we need this?
|
||||
observer.takeRecords();
|
||||
observer.disconnect();
|
||||
observing = false;
|
||||
}
|
||||
|
||||
function run(fn, resolve, wait) {
|
||||
if (document.documentElement) {
|
||||
resolve(fn());
|
||||
return true;
|
||||
}
|
||||
if (wait) wait(fn, resolve);
|
||||
}
|
||||
|
||||
function waitForRoot(...args) {
|
||||
new MutationObserver((_, observer) => run(...args) && observer.disconnect())
|
||||
.observe(document, {childList: true});
|
||||
// const copy = document.importNode(el, true);
|
||||
// el.textContent += ' '; // invalidate CSSOM cache
|
||||
document.documentElement.appendChild(style.el);
|
||||
// moving an element resets its 'disabled' state
|
||||
style.el.disabled = !enabled;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function emitUpdate() {
|
||||
if (onUpdate) {
|
||||
onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceAll(styles) {
|
||||
const added = new Set(styles.map(s => s.id));
|
||||
const removed = [];
|
||||
for (const style of list) {
|
||||
if (!added.has(style.id)) {
|
||||
removed.push(style.id);
|
||||
}
|
||||
}
|
||||
// FIXME: is it possible that `docRootObserver` breaks the process?
|
||||
return Promise.all(styles.map(_add))
|
||||
.then(() => {
|
||||
removed.forEach(_remove);
|
||||
emitUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
524
edit.html
524
edit.html
|
@ -1,86 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html id="stylus">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link href="global.css" rel="stylesheet">
|
||||
<link href="global-dark.css" rel="stylesheet">
|
||||
<style id="cm-theme"></style>
|
||||
<link href="edit/edit.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
||||
|
||||
<script src="js/polyfill.js"></script>
|
||||
<script src="js/toolbox.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<script src="js/sections-util.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="edit/codemirror-themes.js"></script> <!-- must precede base.js -->
|
||||
<script src="edit/base.js"></script>
|
||||
<style id="firefox-transitions-bug-suppressor">
|
||||
/* restrict to FF */
|
||||
@supports (-moz-appearance:none) {
|
||||
/* increased specificity to override sane selectors in user styles */
|
||||
html#stylus.firefox #stylus-edit #header *,
|
||||
html#stylus.firefox #stylus-edit #sections * {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/mode/css/css.js"></script>
|
||||
<script src="vendor/codemirror/mode/stylus/stylus.js"></script>
|
||||
|
||||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/addon/edit/closebrackets.js"></script>
|
||||
|
||||
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
||||
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
||||
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
|
||||
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet" />
|
||||
<script src="vendor/codemirror/addon/fold/foldcode.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/indent-fold.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||
|
||||
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet" />
|
||||
<script src="vendor/codemirror/addon/lint/lint.js"></script>
|
||||
|
||||
|
||||
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet" />
|
||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
||||
|
||||
<link href="vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
|
||||
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
|
||||
|
||||
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
|
||||
|
||||
<script src="js/color/color-converter.js"></script>
|
||||
<script src="js/color/color-mimicry.js"></script>
|
||||
<script src="js/color/color-picker.js"></script>
|
||||
<script src="js/color/color-view.js"></script>
|
||||
<script src="js/polyfill.js"></script>
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/worker-util.js"></script>
|
||||
|
||||
<script src="edit/util.js"></script>
|
||||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<link href="edit/global-search.css" rel="stylesheet">
|
||||
<script src="edit/global-search.js"></script>
|
||||
|
||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||
<script src="edit/codemirror-default.js"></script>
|
||||
|
||||
<script src="edit/util.js"></script>
|
||||
<script src="edit/regexp-tester.js"></script>
|
||||
<script src="edit/live-preview.js"></script>
|
||||
<script src="edit/applies-to-line-widget.js"></script>
|
||||
<script src="edit/reroute-hotkeys.js"></script>
|
||||
<script src="edit/codemirror-factory.js"></script>
|
||||
<script src="edit/moz-section-finder.js"></script>
|
||||
<script src="edit/moz-section-widget.js"></script>
|
||||
<script src="edit/linter-manager.js"></script>
|
||||
<script src="edit/colorpicker-helper.js"></script>
|
||||
<script src="edit/beautify.js"></script>
|
||||
<script src="edit/show-keymap-help.js"></script>
|
||||
<script src="edit/refresh-on-view.js"></script>
|
||||
<script src="edit/codemirror-themes.js"></script>
|
||||
|
||||
<script src="edit/source-editor.js"></script>
|
||||
<script src="edit/sections-editor-section.js"></script>
|
||||
<script src="edit/sections-editor.js"></script>
|
||||
<script src="edit/usw-integration.js"></script>
|
||||
|
||||
<script src="edit/edit.js"></script>
|
||||
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
|
||||
<script src="edit/linter.js"></script>
|
||||
<script src="edit/linter-defaults.js"></script>
|
||||
<script src="edit/linter-engines.js"></script>
|
||||
<script src="edit/linter-meta.js"></script>
|
||||
<script src="edit/linter-help-dialog.js"></script>
|
||||
<script src="edit/linter-report.js"></script>
|
||||
<script src="edit/linter-config-dialog.js"></script>
|
||||
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<template data-id="appliesTo">
|
||||
<li class="applies-to-item">
|
||||
<div class="select-resizer">
|
||||
<select name="applies-type" class="applies-type style-contributor">
|
||||
<option value="url" i18n="appliesUrlOption"></option>
|
||||
<option value="url-prefix" i18n="appliesUrlPrefixOption"></option>
|
||||
<option value="domain" i18n="appliesDomainOption"></option>
|
||||
<option value="regexp" i18n="appliesRegexpOption"></option>
|
||||
<option value="url" i18n-text="appliesUrlOption"></option>
|
||||
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
|
||||
<option value="domain" i18n-text="appliesDomainOption"></option>
|
||||
<option value="regexp" i18n-text="appliesRegexpOption"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<div class="applies-value-wrapper">
|
||||
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
|
||||
<a class="remove-applies-to" i18n="appliesRemove, title:appliesRemove" tabindex="0">
|
||||
<a class="remove-applies-to" href="#" i18n-text="appliesRemove" i18n-title="appliesRemove">
|
||||
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
|
||||
</a>
|
||||
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
|
||||
<a class="add-applies-to" href="#" i18n-text="appliesAdd" i18n-title="appliesAdd">
|
||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -88,8 +137,8 @@
|
|||
</template>
|
||||
|
||||
<template data-id="appliesToEverything">
|
||||
<li class="applies-to-everything" i18n="appliesToEverything">
|
||||
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
|
||||
<li class="applies-to-everything" i18n-text="appliesToEverything">
|
||||
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" href="#">
|
||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -99,25 +148,25 @@
|
|||
<div class="section">
|
||||
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
|
||||
<p class="deleted-section">
|
||||
<button class="restore-section" i18n="sectionRestore"></button>
|
||||
<button class="restore-section" i18n-text="sectionRestore"></button>
|
||||
</p>
|
||||
<label i18n="sectionCode" class="code-label"></label>
|
||||
<label i18n-text="sectionCode" class="code-label"></label>
|
||||
<div class="applies-to">
|
||||
<label i18n="appliesLabel, title:appliesHelp" data-cmd="note">
|
||||
<a class="svg-inline-wrapper applies-to-help" tabindex="0">
|
||||
<label i18n-text="appliesLabel">
|
||||
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</label>
|
||||
<ul class="applies-to-list"></ul>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="remove-section" i18n="sectionRemove"></button>
|
||||
<button class="add-section" i18n="long-text:sectionAdd, short-text:genericAdd"></button>
|
||||
<button class="clone-section" i18n="genericClone"></button>
|
||||
<button class="remove-section" i18n-text="sectionRemove"></button>
|
||||
<button class="add-section" i18n-long-text="sectionAdd" i18n-short-text="genericAdd"></button>
|
||||
<button class="clone-section" i18n-text="genericClone"></button>
|
||||
<button class="move-section-up"></button>
|
||||
<button class="move-section-down"></button>
|
||||
<button class="beautify-section" i18n="styleBeautify"></button>
|
||||
<button class="test-regexp" i18n="genericTest"></button>
|
||||
<button class="beautify-section" i18n-text="styleBeautify"></button>
|
||||
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -127,27 +176,27 @@
|
|||
<div data-type="main">
|
||||
<div data-type="content"></div>
|
||||
<div data-type="actions">
|
||||
<a data-action="case" i18n="title:searchCaseSensitive" tabindex="0">Aa</a>
|
||||
<a data-action="prev" i18n="title:genericPrevious" data-hotkey-tooltip="findPrev" tabindex="0">
|
||||
<a data-action="case" i18n-title="searchCaseSensitive" href="#" tabindex="0">Aa</a>
|
||||
<a data-action="prev" i18n-title="genericPrevious" href="#" data-hotkey-tooltip="findPrev" tabindex="0">
|
||||
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
|
||||
</a>
|
||||
<a data-action="next" i18n="title:genericNext" data-hotkey-tooltip="findNext" tabindex="0">
|
||||
<a data-action="next" i18n-title="genericNext" href="#" data-hotkey-tooltip="findNext" tabindex="0">
|
||||
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
|
||||
</a>
|
||||
<a data-action="close" i18n="title:confirmClose" data-hotkey-tooltip="=Esc" tabindex="0">
|
||||
<a data-action="close" i18n-title="confirmClose" href="#" data-hotkey-tooltip="=Esc" tabindex="0">
|
||||
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div data-type="status">
|
||||
<div class="CodeMirror-search-hint" i18n-text="searchRegexp"></div>
|
||||
<div data-type="tally" i18n="title:searchNumberOfResults"></div>
|
||||
<div data-type="tally" i18n-title="searchNumberOfResults"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="clearSearch">
|
||||
<div data-type="hover" i18n="title:confirmDelete">
|
||||
<div data-type="hover" i18n-title="confirmDelete">
|
||||
<svg data-action="clear" class="svg-icon"><use xlink:href="#svg-icon-close"></use></svg>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -156,7 +205,7 @@
|
|||
<div data-type="content">
|
||||
<div data-type="input-wrapper">
|
||||
<textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required
|
||||
i18n="placeholder:search"></textarea>
|
||||
i18n-placeholder="search"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -165,36 +214,36 @@
|
|||
<div data-type="content">
|
||||
<div data-type="input-wrapper">
|
||||
<textarea data-type="replace-from"
|
||||
i18n="placeholder:replace"
|
||||
i18n-placeholder="replace"
|
||||
class="CodeMirror-search-field" rows="1" required
|
||||
spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div data-type="input-wrapper">
|
||||
<textarea data-type="replace-to"
|
||||
i18n="placeholder:replaceWith"
|
||||
i18n-placeholder="replaceWith"
|
||||
class="CodeMirror-search-field" rows="1" required
|
||||
spellcheck="false"></textarea>
|
||||
</div>
|
||||
<button data-action="replace" i18n="replace" disabled></button>
|
||||
<button data-action="replaceAll" i18n="replaceAll" disabled></button>
|
||||
<button data-action="undo" i18n="undo" disabled></button>
|
||||
<button data-action="replace" i18n-text="replace" disabled></button>
|
||||
<button data-action="replaceAll" i18n-text="replaceAll" disabled></button>
|
||||
<button data-action="undo" i18n-text="undo" disabled></button>
|
||||
<!--
|
||||
Using a separate set of buttons because
|
||||
1. FF can display tooltips only when specified on the <button>, ignores the nested <title> in <svg>
|
||||
2. the icon doesn't fill the entire button area so tooltips aren't shown when the edges are hovered
|
||||
-->
|
||||
<button class="hidden" data-action="replace" i18n="title:replace" disabled>
|
||||
<button class="hidden" data-action="replace" i18n-title="replace" disabled>
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="hidden" data-action="replaceAll" i18n="title:replaceAll" disabled>
|
||||
<button class="hidden" data-action="replaceAll" i18n-title="replaceAll" disabled>
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="15.8,1.8 8.8,8.8 5.2,5.3 3.5,6.9 8.8,12.2 17.5,3.4 "/>
|
||||
<polygon points="15.8,7.8 8.8,14.8 5.2,11.3 3.5,12.9 8.8,18.2 17.5,9.4 "/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="hidden" data-action="undo" i18n="title:undo" disabled>
|
||||
<button class="hidden" data-action="undo" i18n-title="undo" disabled>
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<path d="M11.3,5.5H8.7V1.4L1.9,6.5l6.8,5.1V7.5h2.6c1.8,0,3.2,1.4,3.2,3.2s-1.4,3.2-3.2,3.2H7.8v2h3.5c2.9,0,5.2-2.3,5.2-5.2S14.2,5.5,11.3,5.5z"/>
|
||||
</svg>
|
||||
|
@ -203,7 +252,7 @@
|
|||
</template>
|
||||
|
||||
<template data-id="jumpToLine">
|
||||
<span i18n="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
||||
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
||||
</template>
|
||||
|
||||
<template data-id="regexpTestPartial">
|
||||
|
@ -211,15 +260,15 @@
|
|||
</template>
|
||||
|
||||
<template data-id="resizeGrip">
|
||||
<div class="resize-grip" i18n="title:cm_resizeGripHint"></div>
|
||||
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div>
|
||||
</template>
|
||||
|
||||
<template data-id="keymapHelp">
|
||||
<table class="keymap-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input i18n="placeholder:helpKeyMapHotkey" type="search"></th>
|
||||
<th><input i18n="placeholder:helpKeyMapCommand" type="search"></th>
|
||||
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th>
|
||||
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -230,225 +279,185 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</head>
|
||||
|
||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
||||
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet">
|
||||
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet">
|
||||
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet">
|
||||
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||
<link href="js/color/color-picker.css" rel="stylesheet">
|
||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||
|
||||
<template data-id="body"> <!-- https://crbug.com/1288447 -->
|
||||
<body id="stylus-edit">
|
||||
<div id="header">
|
||||
<h1 id="heading" i18n="data-edit:editStyleHeading, data-add:addStyleTitle">
|
||||
<a class="usercss-only"
|
||||
href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||
i18n="title:externalUsercssDocument" target="_blank">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</h1>
|
||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" spellcheck="false">
|
||||
<a id="reset-name" i18n="title:customNameResetHint" tabindex="0" hidden>
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||
</svg>
|
||||
</a>
|
||||
<input id="name" class="style-contributor" spellcheck="false" required>
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
<label id="enabled-label"
|
||||
i18n="styleEnabledLabel, title:toggleStyle"
|
||||
i18n-text="styleEnabledLabel"
|
||||
i18n-title="toggleStyle"
|
||||
data-hotkey-tooltip="toggleStyle">
|
||||
<input type="checkbox" id="enabled" class="style-contributor">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<label id="preview-label" i18n="previewLabel, title:previewTooltip">
|
||||
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
|
||||
<input type="checkbox" id="editor.livePreview">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<label id="disableAll-label" i18n="data-on:disableAllStyles, data-off:disableAllStylesOff">
|
||||
<input id="disableAll" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<span id="preview-errors" hidden>!</span>
|
||||
<span id="preview-errors" class="hidden">!</span>
|
||||
</div>
|
||||
</section>
|
||||
<section id="actions">
|
||||
<div class="buttons">
|
||||
<div class="split-btn">
|
||||
<button id="save-button" i18n="styleSaveLabel" data-hotkey-tooltip="save" disabled></button
|
||||
><button class="split-btn-pedal usercss-only" i18n="menu-tpl:saveAsTemplate"></button>
|
||||
</div>
|
||||
<button id="beautify" i18n="styleBeautify"></button>
|
||||
<button id="style-settings-btn" i18n="settings"></button>
|
||||
<button id="cancel-button" i18n="title:styleCancelEditLabel">↩</button>
|
||||
<div>
|
||||
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save"></button>
|
||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||
</div>
|
||||
<div id="mozilla-format-buttons" class="buttons sectioned-only">
|
||||
<button id="from-mozilla" i18n="importLabel"></button>
|
||||
<button id="to-mozilla" i18n="exportLabel"></button>
|
||||
<a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0"
|
||||
i18n="title:styleMozillaFormatHeading">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
<div id="mozilla-format-container">
|
||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading">
|
||||
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</h2>
|
||||
<div id="mozilla-format-buttons">
|
||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="details-wrapper">
|
||||
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
|
||||
<summary><h2 id="options-heading" i18n="editorSettings"></h2></summary>
|
||||
<div id="options-wrapper">
|
||||
<div class="options-column">
|
||||
<div class="option">
|
||||
<label id="lineWrapping-label" i18n="cm_lineWrapping">
|
||||
<input id="editor.lineWrapping" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<details id="options" data-pref="editor.options.expanded">
|
||||
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
||||
<div id="options-wrapper">
|
||||
<div class="options-column">
|
||||
<div class="option">
|
||||
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
|
||||
<input id="editor.lineWrapping" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label id="smartIndent-label" i18n-text="cm_smartIndent">
|
||||
<input id="editor.smartIndent" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label id="indentWithTabs-label" i18n-text="cm_indentWithTabs">
|
||||
<input id="editor.indentWithTabs" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n-text="cm_autoCloseBrackets" i18n-title="cm_autoCloseBracketsTooltip">
|
||||
<input id="editor.autoCloseBrackets" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n-text="cm_autocompleteOnTyping">
|
||||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n-text="cm_selectByTokens"
|
||||
i18n-title="cm_selectByTokensTooltip">
|
||||
<input id="editor.selectByTokens" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n-text="cm_colorpicker">
|
||||
<input id="editor.colorpicker" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<a id="colorpicker-settings" href="#" class="svg-inline-wrapper" i18n-title="shortcutsNote" tabindex="0">
|
||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="option usercss-only">
|
||||
<label i18n-text="appliesLineWidgetLabel" i18n-title="appliesLineWidgetWarning">
|
||||
<input id="editor.appliesToLineWidget" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-column">
|
||||
<div class="option aligned">
|
||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||
<input id="editor.tabSize" type="number" min="0">
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.keyMap"></select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label id="smartIndent-label" i18n="cm_smartIndent">
|
||||
<input id="editor.smartIndent" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label id="indentWithTabs-label" i18n="cm_indentWithTabs">
|
||||
<input id="editor.indentWithTabs" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n="cm_autoCloseBrackets, title:cm_autoCloseBracketsTooltip">
|
||||
<input id="editor.autoCloseBrackets" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n="cm_autocompleteOnTyping">
|
||||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n="cm_selectByTokens, title:cm_selectByTokensTooltip">
|
||||
<input id="editor.selectByTokens" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option sectioned-only">
|
||||
<label i18n="cm_arrowKeysTraverse">
|
||||
<input id="editor.arrowKeysTraverse" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label i18n="cm_colorpicker">
|
||||
<input id="editor.colorpicker" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<a id="colorpicker-settings" class="svg-inline-wrapper" i18n="title:shortcutsNote" tabindex="0">
|
||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="option usercss-only">
|
||||
<label i18n="appliesLineWidgetLabel, title:appliesLineWidgetWarning">
|
||||
<input id="editor.appliesToLineWidget" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.theme"></select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-column">
|
||||
<div class="option aligned">
|
||||
<label id="tabSize-label" for="editor.tabSize" i18n="cm_tabSize"></label>
|
||||
<input id="editor.tabSize" type="number" min="0">
|
||||
<div class="option aligned">
|
||||
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.matchHighlight">
|
||||
<option i18n-text="cm_matchHighlightToken" value="token">
|
||||
<option i18n-text="cm_matchHighlightSelection" value="selection">
|
||||
<option i18n-text="genericDisabledLabel" value="">
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="keyMap-label" for="editor.keyMap" i18n="cm_keyMap"></label>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.keyMap"></select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<a id="keyMap-help" class="svg-inline-wrapper" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="theme-label" for="editor.theme" i18n="cm_theme"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.theme"></select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="highlight-label" for="editor.matchHighlight" i18n="cm_matchHighlight"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.matchHighlight">
|
||||
<option i18n="cm_matchHighlightToken" value="token">
|
||||
<option i18n="cm_matchHighlightSelection" value="selection">
|
||||
<option i18n="genericDisabledLabel" value="">
|
||||
<select id="editor.linter">
|
||||
<option value="csslint" selected>CSSLint</option>
|
||||
<option value="stylelint">Stylelint</option>
|
||||
<option value="" i18n-text="genericDisabledLabel"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="linter-label" for="editor.linter" i18n="cm_linter"></label>
|
||||
<div class="select-resizer">
|
||||
<select id="editor.linter">
|
||||
<option value="csslint" selected>CSSLint</option>
|
||||
<option value="stylelint">Stylelint</option>
|
||||
<option value="" i18n="genericDisabledLabel"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<a id="linter-settings" class="svg-inline-wrapper" i18n="title:linterConfigTooltip" tabindex="0">
|
||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
|
||||
<summary><h2 i18n="publish"></h2></summary>
|
||||
<div>
|
||||
<a id="usw-url" href="https://userstyles.world" target="_blank"> </a>
|
||||
<div id="usw-link-info">
|
||||
<dl><dt i18n="styleName"></dt><dd data-usw="name"></dd></dl>
|
||||
<dl><dt i18n="genericDescription"></dt><dd data-usw="description"></dd></dl>
|
||||
</div>
|
||||
<div>
|
||||
<button id="usw-publish-style"
|
||||
i18n="data-publish:publishStyle, data-push:publishPush"></button>
|
||||
<button id="usw-disconnect" i18n="optionsSyncDisconnect"></button>
|
||||
<span id="usw-progress"></span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">
|
||||
<summary><h2 i18n="sections"></h2></summary>
|
||||
<ol id="toc"></ol>
|
||||
</details>
|
||||
<details id="lint" data-pref="editor.lint.expanded" class="ignore-pref-if-compact" hidden>
|
||||
<summary>
|
||||
<h2><span i18n="linterIssues"></span><span id="issue-count"></span>
|
||||
<a id="lint-help" class="svg-inline-wrapper intercepts-click" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
<a id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
|
||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
||||
</a>
|
||||
</h2>
|
||||
</summary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="lint" class="hidden-unless-compact" data-pref="editor.lint.expanded">
|
||||
<summary>
|
||||
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
|
||||
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</h2>
|
||||
</summary>
|
||||
<div class="lint-scroll-container">
|
||||
<div class="lint-report-container"></div>
|
||||
</details>
|
||||
</div>
|
||||
<div id="header-resizer" i18n="title:headerResizerHint"></div>
|
||||
</div>
|
||||
</details>
|
||||
<div id="footer" class="hidden">
|
||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||
i18n="externalUsercssDocument"
|
||||
i18n-text="externalUsercssDocument"
|
||||
target="_blank"></a>
|
||||
</div>
|
||||
</div>
|
||||
<section id="sections"></section>
|
||||
<section id="sections">
|
||||
<!--
|
||||
It seems that we don't use these anymore
|
||||
https://github.com/openstyles/stylus/blob/5cbe8a8d780a6eb9fce11d5846e92bf244c3a3f3/edit/sections.js#L18
|
||||
-->
|
||||
<!-- <h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
|
||||
<a id="sections-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</h2> -->
|
||||
</section>
|
||||
<div id="help-popup">
|
||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||
<div class="contents"></div>
|
||||
|
@ -460,10 +469,8 @@
|
|||
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n="alt:helpAlt">
|
||||
<circle cx="7" cy="5" r="1"/>
|
||||
<path d="M8,8c0-0.5-0.5-1-1-1H6C5.5,7,5,7.4,5,8h1v3c0,0.5,0.5,1,1,1h1c0.5,0,1-0.4,1-1H8V8z"/>
|
||||
<path d="M7,1c3.9,0,7,3.1,7,7s-3.1,7-7,7s-7-3.1-7-7S3.1,1,7,1z M7,2.3C3.9,2.3,1.3,4.9,1.3,8s2.6,5.7,5.7,5.7s5.7-2.6,5.7-5.7S10.1,2.3,7,2.3C7,2.3,7,2.3,7,2.3z"/>
|
||||
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n-alt="helpAlt">
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-close" viewBox="0 0 12 16">
|
||||
|
@ -474,8 +481,8 @@
|
|||
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-config" viewBox="0 0 16 16">
|
||||
<path d="M13.3,12.8l1.5-2.6l-2.2-1.5c0-0.2,0.1-0.5,0.1-0.7c0-0.2,0-0.5-0.1-0.7l2.2-1.5l-1.5-2.6l-2.4,1.2 c-0.4-0.3-0.8-0.5-1.2-0.7L9.5,1h-3L6.3,3.7C5.9,3.8,5.5,4.1,5.1,4.4L2.7,3.2L1.2,5.8l2.2,1.5c0,0.2-0.1,0.5-0.1,0.7 c0,0.2,0,0.5,0.1,0.7l-2.2,1.5l1.5,2.6l2.4-1.2c0.4,0.3,0.8,0.5,1.2,0.7L6.5,15h3l0.2-2.7c0.4-0.2,0.8-0.4,1.2-0.7L13.3,12.8z M8,10.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,1-2.3,2.3-2.3c1.3,0,2.3,1,2.3,2.3C10.3,9.3,9.3,10.3,8,10.3z"/>
|
||||
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
|
||||
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
||||
|
@ -487,21 +494,14 @@
|
|||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-plus" viewBox="0 0 8 8">
|
||||
<path d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
|
||||
<path fill-rule="evenodd" d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-minus" viewBox="0 0 8 8">
|
||||
<path d="M0 3v2h8v-2h-8z"/>
|
||||
<path fill-rule="evenodd" d="M0 3v2h8v-2h-8z"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<link href="edit/edit.css" rel="stylesheet">
|
||||
<script src="js/dark-themer.js"></script> <!-- must be last in HEAD to avoid FOUC -->
|
||||
</head>
|
||||
|
||||
<body id="stylus-edit">
|
||||
<script src="edit/edit.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
590
edit/applies-to-line-widget.js
Normal file
590
edit/applies-to-line-widget.js
Normal file
|
@ -0,0 +1,590 @@
|
|||
/* global regExpTester debounce messageBox CodeMirror template colorMimicry msg
|
||||
$ $create t prefs tryCatch deepEqual */
|
||||
/* exported createAppliesToLineWidget */
|
||||
'use strict';
|
||||
|
||||
function createAppliesToLineWidget(cm) {
|
||||
const THROTTLE_DELAY = 400;
|
||||
const RX_SPACE = /(?:\s+|\/\*)+/y;
|
||||
let TPL, EVENTS, CLICK_ROUTE;
|
||||
let widgets = [];
|
||||
let fromLine, toLine, actualStyle;
|
||||
let initialized = false;
|
||||
return {toggle};
|
||||
|
||||
function toggle(newState = !initialized) {
|
||||
newState = Boolean(newState);
|
||||
if (newState !== initialized) {
|
||||
if (newState) {
|
||||
init();
|
||||
} else {
|
||||
uninit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
initialized = true;
|
||||
|
||||
TPL = {
|
||||
container:
|
||||
$create('div.applies-to', [
|
||||
$create('label', t('appliesLabel')),
|
||||
$create('ul.applies-to-list'),
|
||||
]),
|
||||
listItem: template.appliesTo.cloneNode(true),
|
||||
appliesToEverything:
|
||||
$create('li.applies-to-everything', t('appliesToEverything')),
|
||||
};
|
||||
|
||||
$('.applies-value', TPL.listItem).insertAdjacentElement('afterend',
|
||||
$create('button.test-regexp', t('styleRegexpTestButton')));
|
||||
|
||||
CLICK_ROUTE = {
|
||||
'.test-regexp': showRegExpTester,
|
||||
|
||||
'.remove-applies-to': (item, apply, event) => {
|
||||
event.preventDefault();
|
||||
const applies = item.closest('.applies-to').__applies;
|
||||
const i = applies.indexOf(apply);
|
||||
let repl;
|
||||
let from;
|
||||
let to;
|
||||
if (applies.length < 2) {
|
||||
messageBox({
|
||||
contents: t('appliesRemoveError'),
|
||||
buttons: [t('confirmClose')]
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (i === 0) {
|
||||
from = apply.mark.find().from;
|
||||
to = applies[i + 1].mark.find().from;
|
||||
repl = '';
|
||||
} else if (i === applies.length - 1) {
|
||||
from = applies[i - 1].mark.find().to;
|
||||
to = apply.mark.find().to;
|
||||
repl = '';
|
||||
} else {
|
||||
from = applies[i - 1].mark.find().to;
|
||||
to = applies[i + 1].mark.find().from;
|
||||
repl = ', ';
|
||||
}
|
||||
cm.replaceRange(repl, from, to, 'appliesTo');
|
||||
clearApply(apply);
|
||||
item.remove();
|
||||
applies.splice(i, 1);
|
||||
},
|
||||
|
||||
'.add-applies-to': (item, apply, event) => {
|
||||
event.preventDefault();
|
||||
const applies = item.closest('.applies-to').__applies;
|
||||
const i = applies.indexOf(apply);
|
||||
const pos = apply.mark.find().to;
|
||||
const text = `, ${apply.type.text}("")`;
|
||||
cm.replaceRange(text, pos, pos, 'appliesTo');
|
||||
const newApply = createApply(
|
||||
cm.indexFromPos(pos) + 2,
|
||||
apply.type.text,
|
||||
'',
|
||||
true
|
||||
);
|
||||
setupApplyMarkers(newApply);
|
||||
applies.splice(i + 1, 0, newApply);
|
||||
item.insertAdjacentElement('afterend', buildChildren(applies, newApply));
|
||||
},
|
||||
};
|
||||
|
||||
EVENTS = {
|
||||
onchange({target}) {
|
||||
const typeElement = target.closest('.applies-type');
|
||||
if (typeElement) {
|
||||
const item = target.closest('.applies-to-item');
|
||||
const apply = item.__apply;
|
||||
changeItem(item, apply, 'type', typeElement.value);
|
||||
item.dataset.type = apply.type.text;
|
||||
} else {
|
||||
return EVENTS.oninput.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
oninput({target}) {
|
||||
if (target.matches('.applies-value')) {
|
||||
const item = target.closest('.applies-to-item');
|
||||
const apply = item.__apply;
|
||||
changeItem(item, apply, 'value', target.value);
|
||||
}
|
||||
},
|
||||
onclick(event) {
|
||||
const {target} = event;
|
||||
for (const selector in CLICK_ROUTE) {
|
||||
const routed = target.closest(selector);
|
||||
if (routed) {
|
||||
const item = routed.closest('.applies-to-item');
|
||||
CLICK_ROUTE[selector].call(routed, item, item.__apply, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
actualStyle = $create('style');
|
||||
fromLine = 0;
|
||||
toLine = cm.doc.size;
|
||||
|
||||
cm.on('change', onChange);
|
||||
cm.on('optionChange', onOptionChange);
|
||||
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
requestAnimationFrame(updateWidgetStyle);
|
||||
update();
|
||||
}
|
||||
|
||||
function uninit() {
|
||||
initialized = false;
|
||||
|
||||
widgets.forEach(clearWidget);
|
||||
widgets.length = 0;
|
||||
cm.off('change', onChange);
|
||||
cm.off('optionChange', onOptionChange);
|
||||
msg.off(onRuntimeMessage);
|
||||
actualStyle.remove();
|
||||
}
|
||||
|
||||
function onChange(cm, event) {
|
||||
const {from, to, origin} = event;
|
||||
if (origin === 'appliesTo') {
|
||||
return;
|
||||
}
|
||||
const lastChanged = CodeMirror.changeEnd(event).line;
|
||||
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
|
||||
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
|
||||
if (origin === 'setValue') {
|
||||
update();
|
||||
} else {
|
||||
debounce(update, THROTTLE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
function onOptionChange(cm, option) {
|
||||
if (option === 'theme') {
|
||||
updateWidgetStyle();
|
||||
}
|
||||
}
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
if (msg.reason === 'editPreview' && !$(`#stylus-${msg.style.id}`)) {
|
||||
// no style element with this id means the style doesn't apply to the editor URL
|
||||
return;
|
||||
}
|
||||
if (msg.style || msg.styles ||
|
||||
msg.prefs && 'disableAll' in msg.prefs ||
|
||||
msg.method === 'styleDeleted') {
|
||||
requestAnimationFrame(updateWidgetStyle);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
const changed = {fromLine, toLine};
|
||||
fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
|
||||
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
|
||||
const visible = {fromLine, toLine};
|
||||
const {curOp} = cm;
|
||||
if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
|
||||
if (!curOp) cm.startOperation();
|
||||
doUpdate();
|
||||
if (!curOp) cm.endOperation();
|
||||
}
|
||||
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
|
||||
setTimeout(updateInvisible, 0, changed, visible);
|
||||
}
|
||||
}
|
||||
|
||||
function updateInvisible(changed, visible) {
|
||||
let inOp = false;
|
||||
if (changed.fromLine < visible.fromLine) {
|
||||
fromLine = Math.min(fromLine, changed.fromLine);
|
||||
toLine = Math.min(changed.toLine, visible.fromLine);
|
||||
inOp = true;
|
||||
cm.startOperation();
|
||||
doUpdate();
|
||||
}
|
||||
if (changed.toLine > visible.toLine) {
|
||||
fromLine = Math.max(fromLine, changed.toLine);
|
||||
toLine = Math.max(changed.toLine, visible.toLine);
|
||||
if (!inOp) {
|
||||
inOp = true;
|
||||
cm.startOperation();
|
||||
}
|
||||
doUpdate();
|
||||
}
|
||||
if (inOp) {
|
||||
cm.endOperation();
|
||||
}
|
||||
}
|
||||
|
||||
function updateWidgetStyle() {
|
||||
if (prefs.get('editor.theme') !== 'default' &&
|
||||
!tryCatch(() => $('#cm-theme').sheet.cssRules)) {
|
||||
requestAnimationFrame(updateWidgetStyle);
|
||||
return;
|
||||
}
|
||||
const MIN_LUMA = .05;
|
||||
const MIN_LUMA_DIFF = .4;
|
||||
const color = {
|
||||
wrapper: colorMimicry.get(cm.display.wrapper),
|
||||
gutter: colorMimicry.get(cm.display.gutters, {
|
||||
bg: 'backgroundColor',
|
||||
border: 'borderRightColor',
|
||||
}),
|
||||
line: colorMimicry.get('.CodeMirror-linenumber', null, cm.display.lineDiv),
|
||||
comment: colorMimicry.get('span.cm-comment', null, cm.display.lineDiv),
|
||||
};
|
||||
const hasBorder =
|
||||
color.gutter.style.borderRightWidth !== '0px' &&
|
||||
!/transparent|\b0\)/g.test(color.gutter.style.borderRightColor);
|
||||
const diff = {
|
||||
wrapper: Math.abs(color.gutter.bgLuma - color.wrapper.foreLuma),
|
||||
border: hasBorder ? Math.abs(color.gutter.bgLuma - color.gutter.borderLuma) : 0,
|
||||
line: Math.abs(color.gutter.bgLuma - color.line.foreLuma),
|
||||
};
|
||||
const preferLine = diff.line > diff.wrapper || diff.line > MIN_LUMA_DIFF;
|
||||
const fore = preferLine ? color.line.fore : color.wrapper.fore;
|
||||
|
||||
const border = fore.replace(/[\d.]+(?=\))/, MIN_LUMA_DIFF / 2);
|
||||
const borderStyleForced = `1px ${hasBorder ? color.gutter.style.borderRightStyle : 'solid'} ${border}`;
|
||||
|
||||
actualStyle.textContent = `
|
||||
.applies-to {
|
||||
background-color: ${color.gutter.bg};
|
||||
border-top: ${borderStyleForced};
|
||||
border-bottom: ${borderStyleForced};
|
||||
}
|
||||
.applies-to label {
|
||||
color: ${fore};
|
||||
}
|
||||
.applies-to input,
|
||||
.applies-to button,
|
||||
.applies-to select {
|
||||
background: rgba(255, 255, 255, ${
|
||||
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
|
||||
});
|
||||
border: ${borderStyleForced};
|
||||
transition: none;
|
||||
color: ${fore};
|
||||
}
|
||||
.applies-to .svg-icon.select-arrow {
|
||||
fill: ${fore};
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
document.documentElement.appendChild(actualStyle);
|
||||
}
|
||||
|
||||
function doUpdate() {
|
||||
// find which widgets needs to be update
|
||||
// some widgets (lines) might be deleted
|
||||
widgets = widgets.filter(w => w.line.lineNo() !== null);
|
||||
let i = widgets.findIndex(w => w.line.lineNo() > fromLine) - 1;
|
||||
let j = widgets.findIndex(w => w.line.lineNo() > toLine);
|
||||
if (i === -2) {
|
||||
i = widgets.length - 1;
|
||||
}
|
||||
if (j < 0) {
|
||||
j = widgets.length;
|
||||
}
|
||||
|
||||
// decide search range
|
||||
const fromPos = {line: widgets[i] ? widgets[i].line.lineNo() : 0, ch: 0};
|
||||
const toPos = {line: widgets[j] ? widgets[j].line.lineNo() : toLine + 1, ch: 0};
|
||||
|
||||
// calc index->pos lookup table
|
||||
let index = 0;
|
||||
const lineIndexes = [0];
|
||||
cm.doc.iter(0, toPos.line + 1, ({text}) => {
|
||||
lineIndexes.push((index += text.length + 1));
|
||||
});
|
||||
|
||||
// splice
|
||||
i = Math.max(0, i);
|
||||
widgets.splice(i, 0, ...createWidgets(fromPos, toPos, widgets.splice(i, j - i), lineIndexes));
|
||||
|
||||
fromLine = null;
|
||||
toLine = null;
|
||||
}
|
||||
|
||||
function *createWidgets(start, end, removed, lineIndexes) {
|
||||
let i = 0;
|
||||
let itemHeight;
|
||||
for (const section of findAppliesTo(start, end, lineIndexes)) {
|
||||
let removedWidget = removed[i];
|
||||
while (removedWidget && removedWidget.line.lineNo() < section.pos.line) {
|
||||
clearWidget(removed[i]);
|
||||
removedWidget = removed[++i];
|
||||
}
|
||||
if (removedWidget && deepEqual(removedWidget.node.__applies, section.applies, ['mark'])) {
|
||||
yield removedWidget;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
for (const a of section.applies) {
|
||||
setupApplyMarkers(a, lineIndexes);
|
||||
}
|
||||
if (removedWidget && removedWidget.line.lineNo() === section.pos.line) {
|
||||
// reuse old widget
|
||||
removedWidget.section.applies.forEach(apply => {
|
||||
apply.type.mark.clear();
|
||||
apply.value.mark.clear();
|
||||
});
|
||||
removedWidget.section = section;
|
||||
const newNode = buildElement(section);
|
||||
const removedNode = removedWidget.node;
|
||||
if (removedNode.parentNode) {
|
||||
removedNode.parentNode.replaceChild(newNode, removedNode);
|
||||
}
|
||||
removedWidget.node = newNode;
|
||||
removedWidget.changed();
|
||||
yield removedWidget;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// new widget
|
||||
const widget = cm.addLineWidget(section.pos.line, buildElement(section), {
|
||||
coverGutter: true,
|
||||
noHScroll: true,
|
||||
above: true,
|
||||
height: itemHeight ? section.applies.length * itemHeight : undefined,
|
||||
});
|
||||
widget.section = section;
|
||||
itemHeight = itemHeight || widget.node.offsetHeight / (section.applies.length || 1);
|
||||
yield widget;
|
||||
}
|
||||
removed.slice(i).forEach(clearWidget);
|
||||
}
|
||||
|
||||
function clearWidget(widget) {
|
||||
widget.clear();
|
||||
widget.section.applies.forEach(clearApply);
|
||||
}
|
||||
|
||||
function clearApply(apply) {
|
||||
apply.type.mark.clear();
|
||||
apply.value.mark.clear();
|
||||
apply.mark.clear();
|
||||
}
|
||||
|
||||
function setupApplyMarkers(apply, lineIndexes) {
|
||||
apply.type.mark = cm.markText(
|
||||
posFromIndex(cm, apply.type.start, lineIndexes),
|
||||
posFromIndex(cm, apply.type.end, lineIndexes),
|
||||
{clearWhenEmpty: false}
|
||||
);
|
||||
apply.value.mark = cm.markText(
|
||||
posFromIndex(cm, apply.value.start, lineIndexes),
|
||||
posFromIndex(cm, apply.value.end, lineIndexes),
|
||||
{clearWhenEmpty: false}
|
||||
);
|
||||
apply.mark = cm.markText(
|
||||
posFromIndex(cm, apply.start, lineIndexes),
|
||||
posFromIndex(cm, apply.end, lineIndexes),
|
||||
{clearWhenEmpty: false}
|
||||
);
|
||||
}
|
||||
|
||||
function posFromIndex(cm, index, lineIndexes) {
|
||||
if (!lineIndexes) {
|
||||
return cm.posFromIndex(index);
|
||||
}
|
||||
let line = lineIndexes.prev || 0;
|
||||
const prev = lineIndexes[line];
|
||||
const next = lineIndexes[line + 1];
|
||||
if (prev <= index && index < next) {
|
||||
return {line, ch: index - prev};
|
||||
}
|
||||
let a = index < prev ? 0 : line;
|
||||
let b = index < next ? line + 1 : lineIndexes.length - 1;
|
||||
while (a < b - 1) {
|
||||
const mid = (a + b) >> 1;
|
||||
if (lineIndexes[mid] < index) {
|
||||
a = mid;
|
||||
} else {
|
||||
b = mid;
|
||||
}
|
||||
}
|
||||
line = lineIndexes[b] > index ? a : b;
|
||||
Object.defineProperty(lineIndexes, 'prev', {value: line, configurable: true});
|
||||
return {line, ch: index - lineIndexes[line]};
|
||||
}
|
||||
|
||||
function buildElement({applies}) {
|
||||
const container = TPL.container.cloneNode(true);
|
||||
const list = $('.applies-to-list', container);
|
||||
for (const apply of applies) {
|
||||
list.appendChild(buildChildren(applies, apply));
|
||||
}
|
||||
if (!list.children[0]) {
|
||||
list.appendChild(TPL.appliesToEverything.cloneNode(true));
|
||||
}
|
||||
return Object.assign(container, EVENTS, {__applies: applies});
|
||||
}
|
||||
|
||||
function buildChildren(applies, apply) {
|
||||
const el = TPL.listItem.cloneNode(true);
|
||||
el.dataset.type = apply.type.text;
|
||||
el.__apply = apply;
|
||||
$('.applies-type', el).value = apply.type.text;
|
||||
$('.applies-value', el).value = apply.value.text;
|
||||
return el;
|
||||
}
|
||||
|
||||
function changeItem(itemElement, apply, part, newText) {
|
||||
if (!apply) {
|
||||
return;
|
||||
}
|
||||
part = apply[part];
|
||||
const range = part.mark.find();
|
||||
part.mark.clear();
|
||||
newText = unescapeDoubleslash(newText).replace(/\\/g, '\\\\');
|
||||
cm.replaceRange(newText, range.from, range.to, 'appliesTo');
|
||||
part.mark = cm.markText(
|
||||
range.from,
|
||||
cm.findPosH(range.from, newText.length, 'char'),
|
||||
{clearWhenEmpty: false}
|
||||
);
|
||||
part.text = newText;
|
||||
|
||||
if (part === apply.type) {
|
||||
const range = apply.mark.find();
|
||||
apply.mark.clear();
|
||||
apply.mark = cm.markText(
|
||||
part.mark.find().from,
|
||||
range.to,
|
||||
{clearWhenEmpty: false}
|
||||
);
|
||||
}
|
||||
|
||||
if (apply.type.text === 'regexp' && apply.value.text.trim()) {
|
||||
showRegExpTester(itemElement);
|
||||
}
|
||||
}
|
||||
|
||||
function createApply(pos, typeText, valueText, isQuoted = false) {
|
||||
typeText = typeText.toLowerCase();
|
||||
const start = pos;
|
||||
const typeStart = start;
|
||||
const typeEnd = typeStart + typeText.length;
|
||||
const valueStart = typeEnd + 1 + Number(isQuoted);
|
||||
const valueEnd = valueStart + valueText.length;
|
||||
const end = valueEnd + Number(isQuoted) + 1;
|
||||
return {
|
||||
start,
|
||||
type: {
|
||||
text: typeText,
|
||||
start: typeStart,
|
||||
end: typeEnd,
|
||||
},
|
||||
value: {
|
||||
text: unescapeDoubleslash(valueText),
|
||||
start: valueStart,
|
||||
end: valueEnd,
|
||||
},
|
||||
end
|
||||
};
|
||||
}
|
||||
|
||||
function *findAppliesTo(posStart, posEnd, lineIndexes) {
|
||||
const funcRe = /^(url|url-prefix|domain|regexp)$/i;
|
||||
let pos;
|
||||
const eatToken = sticky => {
|
||||
if (!sticky) skipSpace(pos, posEnd);
|
||||
pos.ch++;
|
||||
const token = cm.getTokenAt(pos, true);
|
||||
pos.ch = token.end;
|
||||
return CodeMirror.cmpPos(pos, posEnd) <= 0 ? token : {};
|
||||
};
|
||||
const docCur = cm.getSearchCursor('@-moz-document', posStart);
|
||||
while (docCur.findNext() &&
|
||||
CodeMirror.cmpPos(docCur.pos.to, posEnd) <= 0) {
|
||||
// CM can be nitpicky at token boundary so we'll check the next character
|
||||
const safePos = {line: docCur.pos.from.line, ch: docCur.pos.from.ch + 1};
|
||||
if (/\b(string|comment)\b/.test(cm.getTokenTypeAt(safePos))) continue;
|
||||
const applies = [];
|
||||
pos = docCur.pos.to;
|
||||
do {
|
||||
skipSpace(pos, posEnd);
|
||||
const funcIndex = lineIndexes[pos.line] + pos.ch;
|
||||
const func = eatToken().string;
|
||||
// no space allowed before the opening parenthesis
|
||||
if (!funcRe.test(func) || eatToken(true).string !== '(') break;
|
||||
const url = eatToken();
|
||||
if (url.type !== 'string' || eatToken().string !== ')') break;
|
||||
const unquotedUrl = unquote(url.string);
|
||||
const apply = createApply(
|
||||
funcIndex,
|
||||
func,
|
||||
unquotedUrl,
|
||||
unquotedUrl !== url.string
|
||||
);
|
||||
applies.push(apply);
|
||||
} while (eatToken().string === ',');
|
||||
yield {
|
||||
pos: docCur.pos.from,
|
||||
applies
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function skipSpace(pos, posEnd) {
|
||||
let {ch, line} = pos;
|
||||
let lookForEnd;
|
||||
line--;
|
||||
cm.doc.iter(pos.line, posEnd.line + 1, ({text}) => {
|
||||
line++;
|
||||
while (true) {
|
||||
if (lookForEnd) {
|
||||
ch = text.indexOf('*/', ch) + 1;
|
||||
if (!ch) {
|
||||
return;
|
||||
}
|
||||
ch++;
|
||||
lookForEnd = false;
|
||||
}
|
||||
// EOL is a whitespace so we'll check the next line
|
||||
if (ch >= text.length) {
|
||||
ch = 0;
|
||||
return;
|
||||
}
|
||||
RX_SPACE.lastIndex = ch;
|
||||
const m = RX_SPACE.exec(text);
|
||||
if (!m) {
|
||||
return true;
|
||||
}
|
||||
ch += m[0].length;
|
||||
lookForEnd = m[0].includes('/*');
|
||||
if (ch < text.length && !lookForEnd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
pos.line = line;
|
||||
pos.ch = ch;
|
||||
}
|
||||
|
||||
function unquote(s) {
|
||||
const first = s.charAt(0);
|
||||
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
|
||||
}
|
||||
|
||||
function unescapeDoubleslash(s) {
|
||||
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/.test(s);
|
||||
return hasSingleEscapes ? s : s.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
function showRegExpTester(item) {
|
||||
regExpTester.toggle(true);
|
||||
regExpTester.update(
|
||||
item.closest('.applies-to').__applies
|
||||
.filter(a => a.type.text === 'regexp')
|
||||
.map(a => unescapeDoubleslash(a.value.text)));
|
||||
}
|
||||
}
|
|
@ -1,274 +0,0 @@
|
|||
/* global CodeMirror */
|
||||
/* global cmFactory */
|
||||
/* global debounce */// toolbox.js
|
||||
/* global editor */
|
||||
/* global linterMan */
|
||||
/* global prefs */
|
||||
'use strict';
|
||||
|
||||
/* Registers 'hint' helper and 'autocompleteOnTyping' option in CodeMirror */
|
||||
|
||||
(() => {
|
||||
const USO_VAR = 'uso-variable';
|
||||
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
|
||||
const USO_INVALID_VAR = 'error ' + USO_VAR;
|
||||
const rxPROP = /^(prop(erty)?|variable-2|string-2)\b/;
|
||||
const rxVAR = /(^|[^-.\w\u0080-\uFFFF])var\(/iyu;
|
||||
const rxCONSUME = /([-\w]*\s*:\s?)?/yu;
|
||||
const cssMime = CodeMirror.mimeModes['text/css'];
|
||||
const docFuncs = addSuffix(cssMime.documentTypes, '(');
|
||||
const {tokenHooks} = cssMime;
|
||||
const originalCommentHook = tokenHooks['/'];
|
||||
const originalHelper = CodeMirror.hint.css || (() => {});
|
||||
let cssMedia, cssProps, cssValues;
|
||||
|
||||
const AOT_ID = 'autocompleteOnTyping';
|
||||
const AOT_PREF_ID = 'editor.' + AOT_ID;
|
||||
const aot = prefs.get(AOT_PREF_ID);
|
||||
CodeMirror.defineOption(AOT_ID, aot, (cm, value) => {
|
||||
cm[value ? 'on' : 'off']('changes', autocompleteOnTyping);
|
||||
cm[value ? 'on' : 'off']('pick', autocompletePicked);
|
||||
});
|
||||
prefs.subscribe(AOT_PREF_ID, (key, val) => cmFactory.globalSetOption(AOT_ID, val), {runNow: aot});
|
||||
|
||||
CodeMirror.registerHelper('hint', 'css', helper);
|
||||
CodeMirror.registerHelper('hint', 'stylus', helper);
|
||||
|
||||
tokenHooks['/'] = tokenizeUsoVariables;
|
||||
|
||||
async function helper(cm) {
|
||||
const pos = cm.getCursor();
|
||||
const {line, ch} = pos;
|
||||
const {styles, text} = cm.getLineHandle(line);
|
||||
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
|
||||
const isLessLang = cm.doc.mode.helperType === 'less';
|
||||
const isStylusLang = cm.doc.mode.name === 'stylus';
|
||||
const type = style && style.split(' ', 1)[0] || 'prop?';
|
||||
if (!type || type === 'comment' || type === 'string') {
|
||||
return originalHelper(cm);
|
||||
}
|
||||
// not using getTokenAt until the need is unavoidable because it reparses text
|
||||
// and runs a whole lot of complex calc inside which is slow on long lines
|
||||
// especially if autocomplete is auto-shown on each keystroke
|
||||
let prev, end, state;
|
||||
let i = index;
|
||||
while (
|
||||
(prev == null || `${styles[i - 1]}`.startsWith(type)) &&
|
||||
(prev = i > 2 ? styles[i - 2] : 0) &&
|
||||
isSameToken(text, style, prev)
|
||||
) i -= 2;
|
||||
i = index;
|
||||
while (
|
||||
(end == null || `${styles[i + 1]}`.startsWith(type)) &&
|
||||
(end = styles[i]) &&
|
||||
isSameToken(text, style, end)
|
||||
) i += 2;
|
||||
const getTokenState = () => state || (state = cm.getTokenAt(pos, true).state.state);
|
||||
const str = text.slice(prev, end);
|
||||
const left = text.slice(prev, ch).trim();
|
||||
let leftLC = left.toLowerCase();
|
||||
let list;
|
||||
switch (leftLC[0]) {
|
||||
|
||||
case '!':
|
||||
list = '!important'.startsWith(leftLC) ? ['!important'] : [];
|
||||
break;
|
||||
|
||||
case '@':
|
||||
list = [
|
||||
'@-moz-document',
|
||||
'@charset',
|
||||
'@font-face',
|
||||
'@import',
|
||||
'@keyframes',
|
||||
'@media',
|
||||
'@namespace',
|
||||
'@page',
|
||||
'@supports',
|
||||
'@viewport',
|
||||
];
|
||||
if (isLessLang) list = findAllCssVars(cm, left, '\\s*:').concat(list);
|
||||
break;
|
||||
|
||||
case '#': // prevents autocomplete for #hex colors
|
||||
break;
|
||||
|
||||
case '-': // --variable
|
||||
case '(': // var(
|
||||
list = str.startsWith('--') || testAt(rxVAR, ch - 5, text)
|
||||
? findAllCssVars(cm, left)
|
||||
: [];
|
||||
if (str.startsWith('(')) {
|
||||
prev++;
|
||||
leftLC = left.slice(1);
|
||||
} else {
|
||||
leftLC = left;
|
||||
}
|
||||
break;
|
||||
|
||||
case '/': // USO vars
|
||||
if (str.startsWith('/*[[') && str.endsWith(']]*/')) {
|
||||
prev += 4;
|
||||
end -= 4;
|
||||
end -= text.slice(end - 4, end) === '-rgb' ? 4 : 0;
|
||||
list = Object.keys((editor.style.usercssData || {}).vars || {}).sort();
|
||||
leftLC = left.slice(4);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'u': // url(), url-prefix()
|
||||
case 'd': // domain()
|
||||
case 'r': // regexp()
|
||||
if (/^(variable|tag|error)/.test(type) &&
|
||||
docFuncs.some(s => s.startsWith(leftLC)) &&
|
||||
/^(top|documentTypes|atBlock)/.test(getTokenState())) {
|
||||
end++;
|
||||
list = docFuncs;
|
||||
break;
|
||||
}
|
||||
// fallthrough to `default`
|
||||
|
||||
default:
|
||||
// property values
|
||||
if (isStylusLang || getTokenState() === 'prop') {
|
||||
while (i > 0 && !rxPROP.test(styles[i + 1])) i -= 2;
|
||||
const propEnd = styles[i];
|
||||
let prop;
|
||||
if (propEnd > text.lastIndexOf(';', ch - 1)) {
|
||||
while (i > 0 && rxPROP.test(styles[i + 1])) i -= 2;
|
||||
prop = text.slice(styles[i] || 0, propEnd).match(/([-\w]+)?$/u)[1];
|
||||
}
|
||||
if (prop) {
|
||||
if (/[^-\w]/.test(leftLC)) {
|
||||
prev += execAt(/[\s:()]*/y, prev, text)[0].length;
|
||||
leftLC = leftLC.replace(/^[^\w\s]\s*/, '');
|
||||
}
|
||||
if (prop.startsWith('--')) prop = 'color'; // assuming 90% of variables are colors
|
||||
if (!cssProps) await initCssProps();
|
||||
list = [...new Set([...cssValues.all[prop] || [], ...cssValues.global])];
|
||||
end = prev + execAt(/(\s*[-a-z(]+)?/y, prev, text)[0].length;
|
||||
}
|
||||
}
|
||||
// properties and media features
|
||||
if (!list &&
|
||||
/^(prop(erty|\?)|atom|error|tag)/.test(type) &&
|
||||
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
|
||||
if (!cssProps) await initCssProps();
|
||||
if (type === 'prop?') {
|
||||
prev += leftLC.length;
|
||||
leftLC = '';
|
||||
}
|
||||
list = state === 'atBlock_parens' ? cssMedia : cssProps;
|
||||
end -= /\W$/u.test(str); // e.g. don't consume ) when inside ()
|
||||
end += execAt(rxCONSUME, end, text)[0].length;
|
||||
|
||||
}
|
||||
if (!list) {
|
||||
return isStylusLang
|
||||
? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus})
|
||||
: originalHelper(cm);
|
||||
}
|
||||
}
|
||||
return {
|
||||
list: (list || []).filter(s => s.startsWith(leftLC)),
|
||||
from: {line, ch: prev + str.match(/^\s*/)[0].length},
|
||||
to: {line, ch: end},
|
||||
};
|
||||
}
|
||||
|
||||
async function initCssProps() {
|
||||
cssValues = await linterMan.worker.getCssPropsValues();
|
||||
cssProps = addSuffix(cssValues.all);
|
||||
cssMedia = [].concat(...Object.entries(cssMime).map(getMediaKeys).filter(Boolean)).sort();
|
||||
}
|
||||
|
||||
function addSuffix(obj, suffix = ': ') {
|
||||
// Sorting first, otherwise "foo-bar:" would precede "foo:"
|
||||
return Object.keys(obj).sort().map(k => k + suffix);
|
||||
}
|
||||
|
||||
function getMediaKeys([k, v]) {
|
||||
return k === 'mediaFeatures' && addSuffix(v) ||
|
||||
k.startsWith('media') && Object.keys(v);
|
||||
}
|
||||
|
||||
/** makes sure we don't process a different adjacent comment */
|
||||
function isSameToken(text, style, i) {
|
||||
return !style || text[i] !== '/' && text[i + 1] !== '*' ||
|
||||
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
|
||||
}
|
||||
|
||||
function findAllCssVars(cm, leftPart, rightPart = '') {
|
||||
// simplified regex without CSS escapes
|
||||
const [, prefixed, named] = leftPart.match(/^(--|@)?(\S)?/);
|
||||
const rx = new RegExp(
|
||||
'(?:^|[\\s/;{])(' +
|
||||
(prefixed ? leftPart : '--') +
|
||||
(named ? '' : '[a-zA-Z_\u0080-\uFFFF]') +
|
||||
'[-0-9a-zA-Z_\u0080-\uFFFF]*)' +
|
||||
rightPart,
|
||||
'g');
|
||||
const list = new Set();
|
||||
cm.eachLine(({text}) => {
|
||||
for (let m; (m = rx.exec(text));) {
|
||||
list.add(m[1]);
|
||||
}
|
||||
});
|
||||
return [...list].sort();
|
||||
}
|
||||
|
||||
function tokenizeUsoVariables(stream) {
|
||||
const token = originalCommentHook.apply(this, arguments);
|
||||
if (token[1] === 'comment') {
|
||||
const {string, start, pos} = stream;
|
||||
if (testAt(/\/\*\[\[/y, start, string) &&
|
||||
testAt(/]]\*\//y, pos - 4, string)) {
|
||||
const vars = (editor.style.usercssData || {}).vars;
|
||||
token[0] =
|
||||
vars && vars.hasOwnProperty(string.slice(start + 4, pos - 4).replace(/-rgb$/, ''))
|
||||
? USO_VALID_VAR
|
||||
: USO_INVALID_VAR;
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function execAt(rx, index, text) {
|
||||
rx.lastIndex = index;
|
||||
return rx.exec(text);
|
||||
}
|
||||
|
||||
function testAt(rx, index, text) {
|
||||
rx.lastIndex = Math.max(0, index);
|
||||
return rx.test(text);
|
||||
}
|
||||
|
||||
function autocompleteOnTyping(cm, [info], debounced) {
|
||||
const lastLine = info.text[info.text.length - 1];
|
||||
if (cm.state.completionActive ||
|
||||
info.origin && !info.origin.includes('input') ||
|
||||
!lastLine) {
|
||||
return;
|
||||
}
|
||||
if (cm.state.autocompletePicked) {
|
||||
cm.state.autocompletePicked = false;
|
||||
return;
|
||||
}
|
||||
if (!debounced) {
|
||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
||||
return;
|
||||
}
|
||||
if (lastLine.match(/[-a-z!]+$/i)) {
|
||||
cm.state.autocompletePicked = false;
|
||||
cm.options.hintOptions.completeSingle = false;
|
||||
cm.execCommand('autocomplete');
|
||||
setTimeout(() => {
|
||||
cm.options.hintOptions.completeSingle = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function autocompletePicked(cm) {
|
||||
cm.state.autocompletePicked = true;
|
||||
}
|
||||
})();
|
427
edit/base.js
427
edit/base.js
|
@ -1,427 +0,0 @@
|
|||
/* global $$ $ $create messageBoxProxy setInputValue setupLivePrefs */// dom.js
|
||||
/* global API */// msg.js
|
||||
/* global CODEMIRROR_THEMES */
|
||||
/* global CodeMirror */
|
||||
/* global MozDocMapper */// sections-util.js
|
||||
/* global chromeSync */// storage-util.js
|
||||
/* global initBeautifyButton */// beautify.js
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
/* global FIREFOX getOwnTab sessionStore tryJSONparse tryURL */// toolbox.js
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @type Editor
|
||||
* @namespace Editor
|
||||
*/
|
||||
const editor = {
|
||||
style: null,
|
||||
dirty: DirtyReporter(),
|
||||
isUsercss: false,
|
||||
isWindowed: false,
|
||||
livePreview: LivePreview(),
|
||||
/** @type {'customName'|'name'} */
|
||||
nameTarget: 'name',
|
||||
previewDelay: 200, // Chrome devtools uses 200
|
||||
saving: false,
|
||||
scrollInfo: null,
|
||||
|
||||
cancel: () => location.assign('/manage.html'),
|
||||
|
||||
updateClass() {
|
||||
$.rootCL.toggle('is-new-style', !editor.style.id);
|
||||
},
|
||||
|
||||
updateTheme(name) {
|
||||
if (!CODEMIRROR_THEMES[name]) {
|
||||
name = 'default';
|
||||
prefs.set('editor.theme', name);
|
||||
}
|
||||
$('#cm-theme').dataset.theme = name;
|
||||
$('#cm-theme').textContent = CODEMIRROR_THEMES[name] || '';
|
||||
},
|
||||
|
||||
updateTitle(isDirty = editor.dirty.isDirty()) {
|
||||
const {customName, name} = editor.style;
|
||||
document.title = `${
|
||||
isDirty ? '* ' : ''
|
||||
}${
|
||||
customName || name || t('styleMissingName')
|
||||
} - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top
|
||||
},
|
||||
};
|
||||
|
||||
//#region pre-init
|
||||
|
||||
(() => {
|
||||
const mqCompact = matchMedia('(max-width: 850px)');
|
||||
const toggleCompact = mq => $.rootCL.toggle('compact-layout', mq.matches);
|
||||
mqCompact.on('change', toggleCompact);
|
||||
toggleCompact(mqCompact);
|
||||
Object.assign(editor, /** @namespace Editor */ {
|
||||
mqCompact,
|
||||
styleReady: prefs.ready.then(loadStyle),
|
||||
});
|
||||
async function loadStyle() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
let id = Number(params.get('id'));
|
||||
const style = id && await API.styles.get(id) || {
|
||||
id: id = null, // resetting the non-existent id
|
||||
name: params.get('domain') ||
|
||||
tryURL(params.get('url-prefix')).hostname ||
|
||||
'',
|
||||
enabled: true,
|
||||
sections: [
|
||||
MozDocMapper.toSection([...params], {code: ''}),
|
||||
],
|
||||
};
|
||||
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
|
||||
const isUC = Boolean(style.usercssData || !id && prefs.get('newStyleAsUsercss'));
|
||||
Object.assign(editor, /** @namespace Editor */ {
|
||||
style,
|
||||
isUsercss: isUC,
|
||||
template: isUC && !id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate), // promise
|
||||
});
|
||||
editor.updateClass();
|
||||
editor.updateTheme(prefs.get('editor.theme'));
|
||||
editor.updateTitle(false);
|
||||
$.rootCL.add(isUC ? 'usercss' : 'sectioned');
|
||||
sessionStore.justEditedStyleId = id || '';
|
||||
// no such style so let's clear the invalid URL parameters
|
||||
if (!id) history.replaceState({}, '', location.pathname);
|
||||
}
|
||||
})();
|
||||
|
||||
//#endregion
|
||||
//#region init header
|
||||
|
||||
/* exported EditorHeader */
|
||||
function EditorHeader() {
|
||||
initBeautifyButton($('#beautify'));
|
||||
initKeymapElement();
|
||||
initNameArea();
|
||||
initThemeElement();
|
||||
setupLivePrefs();
|
||||
|
||||
window.on('load', () => {
|
||||
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
|
||||
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||
}, {once: true});
|
||||
|
||||
function findKeyForCommand(command, map) {
|
||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||
let key = Object.keys(map).find(k => map[k] === command);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
||||
key = ft && findKeyForCommand(command, ft);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function initNameArea() {
|
||||
const nameEl = $('#name');
|
||||
const resetEl = $('#reset-name');
|
||||
const isCustomName = editor.style.updateUrl || editor.isUsercss;
|
||||
editor.nameTarget = isCustomName ? 'customName' : 'name';
|
||||
nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||
nameEl.title = isCustomName ? t('customNameHint') : '';
|
||||
nameEl.on('input', () => {
|
||||
editor.updateName(true);
|
||||
resetEl.hidden = !editor.style.customName;
|
||||
});
|
||||
resetEl.hidden = !editor.style.customName;
|
||||
resetEl.onclick = () => {
|
||||
editor.style.customName = null; // to delete it from db
|
||||
setInputValue(nameEl, editor.style.name);
|
||||
resetEl.hidden = true;
|
||||
};
|
||||
const enabledEl = $('#enabled');
|
||||
enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked);
|
||||
}
|
||||
|
||||
function initThemeElement() {
|
||||
$('#editor.theme').append(...[
|
||||
$create('option', {value: 'default'}, t('defaultTheme')),
|
||||
...Object.keys(CODEMIRROR_THEMES).map(s => $create('option', s)),
|
||||
]);
|
||||
// move the theme after built-in CSS so that its same-specificity selectors win
|
||||
document.head.appendChild($('#cm-theme'));
|
||||
}
|
||||
|
||||
function initKeymapElement() {
|
||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
||||
const maps = Object.keys(CodeMirror.keyMap)
|
||||
.map(name => ({
|
||||
value: name,
|
||||
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
||||
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
||||
}))
|
||||
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let bin = fragment;
|
||||
let groupName;
|
||||
// group suffixed maps in <optgroup>
|
||||
maps.forEach(({value, name}, i) => {
|
||||
groupName = !name.includes('-') ? name : groupName;
|
||||
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
||||
if (groupWithNext) {
|
||||
if (bin === fragment) {
|
||||
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
||||
}
|
||||
}
|
||||
const el = bin.appendChild($create('option', {value}, name));
|
||||
if (value === prefs.defaults['editor.keyMap']) {
|
||||
el.dataset.default = '';
|
||||
el.title = t('defaultTheme');
|
||||
}
|
||||
if (!groupWithNext) bin = fragment;
|
||||
});
|
||||
const selector = $('#editor.keyMap');
|
||||
selector.textContent = '';
|
||||
selector.appendChild(fragment);
|
||||
selector.value = prefs.get('editor.keyMap');
|
||||
}
|
||||
|
||||
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||
for (const el of $$('[data-hotkey-tooltip]')) {
|
||||
if (el._hotkeyTooltipKeyMap !== mapName) {
|
||||
el._hotkeyTooltipKeyMap = mapName;
|
||||
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
||||
const cmd = el.dataset.hotkeyTooltip;
|
||||
const key = cmd[0] === '=' ? cmd.slice(1) :
|
||||
findKeyForCommand(cmd, mapName) ||
|
||||
extraKeys && findKeyForCommand(cmd, extraKeys);
|
||||
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
||||
if (el.title !== newTitle) el.title = newTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region init windowed mode
|
||||
|
||||
(() => {
|
||||
let ownTabId;
|
||||
if (chrome.windows) {
|
||||
initWindowedMode();
|
||||
const pos = tryJSONparse(sessionStore.windowPos);
|
||||
delete sessionStore.windowPos;
|
||||
// resize the window on 'undo close'
|
||||
if (pos && pos.left != null) {
|
||||
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
|
||||
}
|
||||
}
|
||||
|
||||
getOwnTab().then(tab => {
|
||||
ownTabId = tab.id;
|
||||
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
|
||||
editor.cancel = () => history.back();
|
||||
}
|
||||
});
|
||||
|
||||
async function initWindowedMode() {
|
||||
chrome.tabs.onAttached.addListener(onTabAttached);
|
||||
// Chrome 96+ bug: the type is 'app' for a window that was restored via Ctrl-Shift-T
|
||||
const isSimple = ['app', 'popup'].includes((await browser.windows.getCurrent()).type);
|
||||
if (isSimple) require(['/edit/embedded-popup']);
|
||||
editor.isWindowed = isSimple || (
|
||||
history.length === 1 &&
|
||||
await prefs.ready && prefs.get('openEditInWindow') &&
|
||||
(await browser.windows.getAll()).length > 1 &&
|
||||
(await browser.tabs.query({currentWindow: true})).length === 1
|
||||
);
|
||||
}
|
||||
|
||||
async function onTabAttached(tabId, info) {
|
||||
if (tabId !== ownTabId) {
|
||||
return;
|
||||
}
|
||||
if (info.newPosition !== 0) {
|
||||
prefs.set('openEditInWindow', false);
|
||||
return;
|
||||
}
|
||||
const win = await browser.windows.get(info.newWindowId, {populate: true});
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
const openEditInWindow = win.tabs.length === 1;
|
||||
// FF-only because Chrome retardedly resets the size during dragging
|
||||
if (openEditInWindow && FIREFOX) {
|
||||
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
||||
}
|
||||
prefs.set('openEditInWindow', openEditInWindow);
|
||||
}
|
||||
})();
|
||||
|
||||
//#endregion
|
||||
//#region internals
|
||||
|
||||
/** @returns DirtyReporter */
|
||||
function DirtyReporter() {
|
||||
const data = new Map();
|
||||
const listeners = new Set();
|
||||
const dataListeners = new Set();
|
||||
const notifyChange = wasDirty => {
|
||||
const isDirty = data.size > 0;
|
||||
const flipped = isDirty !== wasDirty;
|
||||
if (flipped) {
|
||||
listeners.forEach(cb => cb(isDirty));
|
||||
}
|
||||
if (flipped || isDirty) {
|
||||
dataListeners.forEach(cb => cb(isDirty));
|
||||
}
|
||||
};
|
||||
/** @namespace DirtyReporter */
|
||||
return {
|
||||
add(obj, value) {
|
||||
const wasDirty = data.size > 0;
|
||||
const saved = data.get(obj);
|
||||
if (!saved) {
|
||||
data.set(obj, {type: 'add', newValue: value});
|
||||
} else if (saved.type === 'remove') {
|
||||
if (saved.savedValue === value) {
|
||||
data.delete(obj);
|
||||
} else {
|
||||
saved.newValue = value;
|
||||
saved.type = 'modify';
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
clear(...objs) {
|
||||
if (data.size && (
|
||||
objs.length
|
||||
? objs.map(data.delete, data).includes(true)
|
||||
: (data.clear(), true)
|
||||
)) {
|
||||
notifyChange(true);
|
||||
}
|
||||
},
|
||||
has(key) {
|
||||
return data.has(key);
|
||||
},
|
||||
isDirty() {
|
||||
return data.size > 0;
|
||||
},
|
||||
modify(obj, oldValue, newValue) {
|
||||
const wasDirty = data.size > 0;
|
||||
const saved = data.get(obj);
|
||||
if (!saved) {
|
||||
if (oldValue !== newValue) {
|
||||
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (saved.type === 'modify') {
|
||||
if (saved.savedValue === newValue) {
|
||||
data.delete(obj);
|
||||
} else {
|
||||
saved.newValue = newValue;
|
||||
}
|
||||
} else if (saved.type === 'add') {
|
||||
saved.newValue = newValue;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
onChange(cb, add = true) {
|
||||
listeners[add ? 'add' : 'delete'](cb);
|
||||
},
|
||||
onDataChange(cb, add = true) {
|
||||
dataListeners[add ? 'add' : 'delete'](cb);
|
||||
},
|
||||
remove(obj, value) {
|
||||
const wasDirty = data.size > 0;
|
||||
const saved = data.get(obj);
|
||||
if (!saved) {
|
||||
data.set(obj, {type: 'remove', savedValue: value});
|
||||
} else if (saved.type === 'add') {
|
||||
data.delete(obj);
|
||||
} else if (saved.type === 'modify') {
|
||||
saved.type = 'remove';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function LivePreview() {
|
||||
let el;
|
||||
let data;
|
||||
let port;
|
||||
let preprocess;
|
||||
let enabled = prefs.get('editor.livePreview');
|
||||
|
||||
prefs.subscribe('editor.livePreview', (key, value) => {
|
||||
if (!value) {
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
port = null;
|
||||
}
|
||||
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
|
||||
createPreviewer();
|
||||
updatePreviewer(data);
|
||||
}
|
||||
enabled = value;
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* @param {Function} [fn] - preprocessor
|
||||
*/
|
||||
init(fn) {
|
||||
preprocess = fn;
|
||||
},
|
||||
|
||||
update(newData) {
|
||||
data = newData;
|
||||
if (!port) {
|
||||
if (!data.id || !data.enabled || !enabled) {
|
||||
return;
|
||||
}
|
||||
createPreviewer();
|
||||
}
|
||||
updatePreviewer(data);
|
||||
},
|
||||
};
|
||||
|
||||
function createPreviewer() {
|
||||
port = chrome.runtime.connect({name: 'livePreview'});
|
||||
port.onDisconnect.addListener(err => {
|
||||
throw err;
|
||||
});
|
||||
el = $('#preview-errors');
|
||||
el.onclick = () => messageBoxProxy.alert(el.title, 'pre');
|
||||
}
|
||||
|
||||
async function updatePreviewer(data) {
|
||||
try {
|
||||
port.postMessage(preprocess ? await preprocess(data) : data);
|
||||
el.hidden = true;
|
||||
} catch (err) {
|
||||
if (Array.isArray(err)) {
|
||||
err = err.map(e => e.message || e).join('\n');
|
||||
} else if (err && err.index != null) {
|
||||
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
|
||||
const pos = editor.getEditors()[0].posFromIndex(err.index);
|
||||
err.message = `${pos.line}:${pos.ch} ${err.message || err}`;
|
||||
}
|
||||
el.title = err.message || `${err}`;
|
||||
el.hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
281
edit/beautify.js
281
edit/beautify.js
|
@ -1,174 +1,135 @@
|
|||
/* global $ $create moveFocus */// dom.js
|
||||
/* global CodeMirror */
|
||||
/* global createHotkeyInput helpPopup */// util.js
|
||||
/* global editor */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
/* global loadScript css_beautify showHelp prefs t $ $create */
|
||||
/* exported beautify */
|
||||
'use strict';
|
||||
|
||||
CodeMirror.commands.beautify = cm => {
|
||||
// using per-section mode when code editor or applies-to block is focused
|
||||
const isPerSection = cm.display.wrapper.parentElement.contains(document.activeElement);
|
||||
beautify(isPerSection ? [cm] : editor.getEditors(), false);
|
||||
};
|
||||
function beautify(scope) {
|
||||
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
||||
.then(() => {
|
||||
if (!window.css_beautify && window.exports) {
|
||||
window.css_beautify = window.exports.css_beautify;
|
||||
}
|
||||
})
|
||||
.then(doBeautify);
|
||||
|
||||
prefs.subscribe('editor.beautify.hotkey', (key, value) => {
|
||||
const {extraKeys} = CodeMirror.defaults;
|
||||
for (const [key, cmd] of Object.entries(extraKeys)) {
|
||||
if (cmd === 'beautify') {
|
||||
delete extraKeys[key];
|
||||
break;
|
||||
function doBeautify() {
|
||||
const tabs = prefs.get('editor.indentWithTabs');
|
||||
const options = Object.assign({}, prefs.get('editor.beautify'));
|
||||
for (const k of Object.keys(prefs.defaults['editor.beautify'])) {
|
||||
if (!(k in options)) options[k] = prefs.defaults['editor.beautify'][k];
|
||||
}
|
||||
}
|
||||
if (value) {
|
||||
extraKeys[value] = 'beautify';
|
||||
}
|
||||
}, {runNow: true});
|
||||
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
||||
options.indent_char = tabs ? '\t' : ' ';
|
||||
|
||||
/**
|
||||
* @name beautify
|
||||
* @param {CodeMirror[]} scope
|
||||
* @param {boolean} [ui=true]
|
||||
*/
|
||||
async function beautify(scope, ui = true) {
|
||||
await require(['/vendor-overwrites/beautify/beautify-css-mod']); /* global css_beautify */
|
||||
const tabs = prefs.get('editor.indentWithTabs');
|
||||
const options = Object.assign(prefs.defaults['editor.beautify'], prefs.get('editor.beautify'));
|
||||
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
||||
options.indent_char = tabs ? '\t' : ' ';
|
||||
if (ui) {
|
||||
createBeautifyUI(scope, options);
|
||||
}
|
||||
for (const cm of scope) {
|
||||
setTimeout(beautifyEditor, 0, cm, options, ui);
|
||||
}
|
||||
}
|
||||
showHelp(t('styleBeautify'),
|
||||
$create([
|
||||
$create('.beautify-options', [
|
||||
$createOption('.selector1,', 'selector_separator_newline'),
|
||||
$createOption('.selector2', 'newline_before_open_brace'),
|
||||
$createOption('{', 'newline_after_open_brace'),
|
||||
$createOption('border: none;', 'newline_between_properties', true),
|
||||
$createOption('display: block;', 'newline_before_close_brace', true),
|
||||
$createOption('}', 'newline_between_rules'),
|
||||
$createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'),
|
||||
$createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'),
|
||||
]),
|
||||
$create('.buttons', [
|
||||
$create('button', {
|
||||
attributes: {role: 'close'},
|
||||
// showHelp.close will be defined after showHelp() is invoked
|
||||
onclick: () => showHelp.close(),
|
||||
}, t('confirmClose')),
|
||||
$create('button', {
|
||||
attributes: {role: 'undo'},
|
||||
onclick() {
|
||||
let undoable = false;
|
||||
for (const cm of scope) {
|
||||
const data = cm.beautifyChange;
|
||||
if (!data || !data[cm.changeGeneration()]) continue;
|
||||
delete data[cm.changeGeneration()];
|
||||
const {scrollX, scrollY} = window;
|
||||
cm.undo();
|
||||
cm.scrollIntoView(cm.getCursor());
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
undoable |= data[cm.changeGeneration()];
|
||||
}
|
||||
this.disabled = !undoable;
|
||||
},
|
||||
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
|
||||
]),
|
||||
]));
|
||||
|
||||
function beautifyEditor(cm, options, ui) {
|
||||
const pos = options.translate_positions =
|
||||
[].concat.apply([], cm.doc.sel.ranges.map(r =>
|
||||
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
|
||||
const text = cm.getValue();
|
||||
const newText = css_beautify(text, options);
|
||||
if (newText !== text) {
|
||||
if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
|
||||
// clear the list if last change wasn't a css-beautify
|
||||
cm.beautifyChange = {};
|
||||
}
|
||||
cm.setValue(newText);
|
||||
const selections = [];
|
||||
for (let i = 0; i < pos.length; i += 2) {
|
||||
selections.push({anchor: pos[i], head: pos[i + 1]});
|
||||
}
|
||||
const {scrollX, scrollY} = window;
|
||||
cm.setSelections(selections);
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
cm.beautifyChange[cm.changeGeneration()] = true;
|
||||
if (ui) {
|
||||
$('button[role="close"]', helpPopup.div).disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#help-popup').className = 'wide';
|
||||
|
||||
function createBeautifyUI(scope, options) {
|
||||
helpPopup.show(t('styleBeautify'),
|
||||
$create([
|
||||
$create('.beautify-options', [
|
||||
$createOption('.selector1,', 'selector_separator_newline'),
|
||||
$createOption('.selector2', 'newline_before_open_brace'),
|
||||
$createOption('{', 'newline_after_open_brace'),
|
||||
$createOption('border: none;', 'newline_between_properties', true),
|
||||
$createOption('display: block;', 'newline_before_close_brace', true),
|
||||
$createOption('}', 'newline_between_rules'),
|
||||
$createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'),
|
||||
$createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'),
|
||||
editor.isUsercss && $createLabeledCheckbox('indent_mozdoc', '', '... @-moz-document'),
|
||||
]),
|
||||
$create('p.beautify-hint', [
|
||||
$create('span', t('styleBeautifyHint') + '\u00A0'),
|
||||
createHotkeyInput('editor.beautify.hotkey', {
|
||||
buttons: false,
|
||||
onDone: () => moveFocus(helpPopup.div, 0),
|
||||
}),
|
||||
]),
|
||||
$create('.buttons', [
|
||||
$create('button', {
|
||||
attributes: {role: 'close'},
|
||||
onclick: helpPopup.close,
|
||||
}, t('confirmClose')),
|
||||
$create('button', {
|
||||
attributes: {role: 'undo'},
|
||||
onclick() {
|
||||
let undoable = false;
|
||||
for (const cm of scope) {
|
||||
const data = cm.beautifyChange;
|
||||
if (!data || !data[cm.changeGeneration()]) continue;
|
||||
delete data[cm.changeGeneration()];
|
||||
const {scrollX, scrollY} = window;
|
||||
cm.undo();
|
||||
cm.scrollIntoView(cm.getCursor());
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
undoable |= data[cm.changeGeneration()];
|
||||
}
|
||||
this.disabled = !undoable;
|
||||
},
|
||||
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
|
||||
]),
|
||||
]),
|
||||
{
|
||||
className: 'wide',
|
||||
scope.forEach(cm => {
|
||||
setTimeout(() => {
|
||||
const pos = options.translate_positions =
|
||||
[].concat.apply([], cm.doc.sel.ranges.map(r =>
|
||||
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
|
||||
const text = cm.getValue();
|
||||
const newText = css_beautify(text, options);
|
||||
if (newText !== text) {
|
||||
if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
|
||||
// clear the list if last change wasn't a css-beautify
|
||||
cm.beautifyChange = {};
|
||||
}
|
||||
cm.setValue(newText);
|
||||
const selections = [];
|
||||
for (let i = 0; i < pos.length; i += 2) {
|
||||
selections.push({anchor: pos[i], head: pos[i + 1]});
|
||||
}
|
||||
const {scrollX, scrollY} = window;
|
||||
cm.setSelections(selections);
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
cm.beautifyChange[cm.changeGeneration()] = true;
|
||||
$('#help-popup button[role="close"]').disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.beautify-options').onchange = ({target}) => {
|
||||
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
|
||||
const elLine = target.closest('[newline]');
|
||||
if (elLine) elLine.setAttribute('newline', value);
|
||||
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
|
||||
beautify(scope, false);
|
||||
};
|
||||
$('.beautify-options').onchange = ({target}) => {
|
||||
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
|
||||
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
|
||||
if (target.parentNode.hasAttribute('newline')) {
|
||||
target.parentNode.setAttribute('newline', value.toString());
|
||||
}
|
||||
doBeautify();
|
||||
};
|
||||
|
||||
function $createOption(label, optionName, indent) {
|
||||
const value = options[optionName];
|
||||
return (
|
||||
$create('div', {attributes: {newline: value}}, [
|
||||
$create('span', indent ? {attributes: {indent: ''}} : {}, label),
|
||||
$create('div.select-resizer', [
|
||||
$create('select', {dataset: {option: optionName}}, [
|
||||
$create('option', {selected: !value}, '\xA0'),
|
||||
$create('option', {selected: value}, '\\n'),
|
||||
function $createOption(label, optionName, indent) {
|
||||
const value = options[optionName];
|
||||
return (
|
||||
$create('div', {attributes: {newline: value}}, [
|
||||
$create('span', indent ? {attributes: {indent: ''}} : {}, label),
|
||||
$create('div.select-resizer', [
|
||||
$create('select', {dataset: {option: optionName}}, [
|
||||
$create('option', {selected: !value}, '\xA0'),
|
||||
$create('option', {selected: value}, '\\n'),
|
||||
]),
|
||||
$create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
|
||||
$create('SVG:path', {
|
||||
'fill-rule': 'evenodd',
|
||||
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
|
||||
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z'
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
$create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
|
||||
$create('SVG:path', {
|
||||
'fill-rule': 'evenodd',
|
||||
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
|
||||
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z',
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
);
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function $createLabeledCheckbox(optionName, i18nKey, text) {
|
||||
return (
|
||||
$create('label', {style: 'display: block; clear: both;'}, [
|
||||
$create('input', {
|
||||
type: 'checkbox',
|
||||
dataset: {option: optionName},
|
||||
checked: options[optionName] !== false,
|
||||
}),
|
||||
$create('SVG:svg.svg-icon.checked',
|
||||
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
|
||||
i18nKey ? t(i18nKey) : text,
|
||||
])
|
||||
);
|
||||
function $createLabeledCheckbox(optionName, i18nKey) {
|
||||
return (
|
||||
$create('label', {style: 'display: block; clear: both;'}, [
|
||||
$create('input', {
|
||||
type: 'checkbox',
|
||||
dataset: {option: optionName},
|
||||
checked: options[optionName] !== false
|
||||
}),
|
||||
$create('SVG:svg.svg-icon.checked',
|
||||
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
|
||||
t(i18nKey),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* exported initBeautifyButton */
|
||||
function initBeautifyButton(btn, scope) {
|
||||
btn.onclick = btn.oncontextmenu = e => {
|
||||
e.preventDefault();
|
||||
beautify(scope || editor.getEditors(), e.type === 'click');
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
/* Built-in CodeMirror and addon customization */
|
||||
|
||||
.CodeMirror-hints {
|
||||
z-index: 999;
|
||||
}
|
||||
.CodeMirror-hint:hover {
|
||||
color: var(--bg);
|
||||
color: white;
|
||||
background: #08f;
|
||||
}
|
||||
.CodeMirror {
|
||||
border: solid var(--c80) 1px;
|
||||
transition: box-shadow .1s;
|
||||
border: solid #CCC 1px;
|
||||
}
|
||||
.CodeMirror {
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
border: solid var(--c80) 1px;
|
||||
transition: box-shadow .1s;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
background-color: var(--c95);
|
||||
border-color: var(--c85);
|
||||
}
|
||||
#stylus#stylus .CodeMirror {
|
||||
/* Using a specificity hack to override userstyles */
|
||||
/* Not using the ring-color hack as it became ugly in new Chrome */
|
||||
outline: none !important;
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background: none;
|
||||
}
|
||||
.CodeMirror-dialog {
|
||||
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
||||
-webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
||||
}
|
||||
.CodeMirror-focused {
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
@supports (-moz-appearance:none) {
|
||||
/* restrict to FF */
|
||||
.CodeMirror-focused {
|
||||
outline: #7dadd9 auto 1px;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
.CodeMirror-search-field {
|
||||
width: 10em;
|
||||
|
@ -36,8 +32,12 @@
|
|||
width: 5em;
|
||||
}
|
||||
.CodeMirror-search-hint {
|
||||
color: var(--c50);
|
||||
color: #888;
|
||||
}
|
||||
.cm-uso-variable {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.CodeMirror-activeline .applies-to:before {
|
||||
background-color: hsla(214, 100%, 90%, 0.15);
|
||||
content: "";
|
||||
|
@ -48,9 +48,11 @@
|
|||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.CodeMirror-activeline .applies-to ul {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter-open::after,
|
||||
.CodeMirror-foldgutter-folded::after {
|
||||
top: 5px;
|
||||
|
@ -62,87 +64,15 @@
|
|||
opacity: .5;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter-open::after {
|
||||
border-width: 5px 3px 0 3px;
|
||||
border-color: currentColor transparent transparent transparent;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter-folded::after {
|
||||
margin-top: -2px;
|
||||
margin-left: 1px;
|
||||
border-width: 4px 0 4px 5px;
|
||||
border-color: transparent transparent transparent currentColor;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
cursor: pointer; /* for bookmarking */
|
||||
}
|
||||
.cm-matchhighlight,
|
||||
.CodeMirror-selection-highlight-scrollbar {
|
||||
background: hsla(200, 100%, 50%, var(--match-hl-opacity, .1));
|
||||
}
|
||||
|
||||
/* Custom stuff we add to CodeMirror */
|
||||
|
||||
.cm-uso-variable {
|
||||
font-weight: bold;
|
||||
}
|
||||
.gutter-bookmark {
|
||||
background: linear-gradient(0deg, hsla(180, 100%, 30%, .75) 2px, hsla(180, 100%, 30%, .2) 2px);
|
||||
}
|
||||
|
||||
@media screen and (prefers-color-scheme: dark), dark {
|
||||
.CodeMirror {
|
||||
--match-hl-opacity: .18;
|
||||
}
|
||||
.CodeMirror-dialog {
|
||||
background-color: #333;
|
||||
}
|
||||
.CodeMirror-dialog-top {
|
||||
border-color: #555;
|
||||
}
|
||||
.CodeMirror-activeline-background {
|
||||
background: hsl(180, 21%, 18%);
|
||||
}
|
||||
.CodeMirror-selected,
|
||||
.CodeMirror-focused .CodeMirror-selected,
|
||||
.CodeMirror-line::selection,
|
||||
.CodeMirror-line > span::selection,
|
||||
.CodeMirror-line > span > span::selection {
|
||||
background: #444;
|
||||
}
|
||||
.CodeMirror-line::-moz-selection,
|
||||
.CodeMirror-line > span::-moz-selection,
|
||||
.CodeMirror-line > span > span::-moz-selection {
|
||||
/* TODO: remove this when strict_min_version >= 62 */
|
||||
background: #444;
|
||||
}
|
||||
.cm-s-default div.CodeMirror-cursor {
|
||||
border-left: 1px solid #fff;
|
||||
}
|
||||
/* Using Chromium's dark devtools colors */
|
||||
.cm-s-default .cm-atom,
|
||||
.cm-s-default .cm-number { color: #a1f7b5 }
|
||||
.cm-s-default .cm-attribute { color: #6194c6 }
|
||||
.cm-s-default .cm-bracket { color: #997 }
|
||||
.cm-s-default .cm-builtin,
|
||||
.cm-s-default .cm-link { color: #9fb4d6 }
|
||||
.cm-s-default .cm-comment { color: #747474 }
|
||||
.cm-s-default .cm-qualifier { color: #ffa34f }
|
||||
.cm-s-default .cm-def,
|
||||
.cm-s-default .cm-header,
|
||||
.cm-s-default .cm-tag,
|
||||
.cm-s-default .cm-type { color: #5db0d7 }
|
||||
.cm-s-default .cm-hr { color: #999 }
|
||||
.cm-s-default .cm-keyword { color: #9a7fd5 }
|
||||
.cm-s-default .cm-meta { color: #ddfb55 }
|
||||
.cm-s-default .cm-operator { color: #d2c057 }
|
||||
.cm-s-default .cm-string { color: #f28b54 }
|
||||
.cm-s-default .cm-variable { color: #d9d9d9 }
|
||||
.cm-s-default .cm-variable-2 { color: #72b9ff }
|
||||
.cm-s-default .cm-variable-3 { color: #9bbbdc }
|
||||
|
||||
@keyframes highlight {
|
||||
from {
|
||||
background-color: #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
/* global $ */// dom.js
|
||||
/* global CodeMirror */
|
||||
/* global UA */// toolbox.js
|
||||
/* global editor */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
/* global CodeMirror prefs loadScript editor $ template */
|
||||
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
(function () {
|
||||
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
||||
if (!prefs.get('editor.keyMap')) {
|
||||
prefs.reset('editor.keyMap');
|
||||
|
@ -26,11 +22,11 @@
|
|||
matchBrackets: true,
|
||||
hintOptions: {},
|
||||
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
||||
styleActiveLine: {nonEmpty: true},
|
||||
styleActiveLine: true,
|
||||
theme: prefs.get('editor.theme'),
|
||||
keyMap: prefs.get('editor.keyMap'),
|
||||
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
||||
// independent of current keyMap; some are implemented only for the edit page
|
||||
// independent of current keyMap
|
||||
'Alt-Enter': 'toggleStyle',
|
||||
'Alt-PageDown': 'nextEditor',
|
||||
'Alt-PageUp': 'prevEditor',
|
||||
|
@ -41,108 +37,387 @@
|
|||
|
||||
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
||||
|
||||
// Adding hotkeys to some keymaps except 'basic' which is primitive by design
|
||||
{
|
||||
const KM = CodeMirror.keyMap;
|
||||
const extras = Object.values(CodeMirror.defaults.extraKeys);
|
||||
if (!extras.includes('jumpToLine')) {
|
||||
KM.sublime['Ctrl-G'] = 'jumpToLine';
|
||||
KM.emacsy['Ctrl-G'] = 'jumpToLine';
|
||||
KM.pcDefault['Ctrl-J'] = 'jumpToLine';
|
||||
KM.macDefault['Cmd-J'] = 'jumpToLine';
|
||||
}
|
||||
if (!extras.includes('autocomplete')) {
|
||||
// will be used by 'sublime' on PC via fallthrough
|
||||
KM.pcDefault['Ctrl-Space'] = 'autocomplete';
|
||||
// OSX uses Ctrl-Space and Cmd-Space for something else
|
||||
KM.macDefault['Alt-Space'] = 'autocomplete';
|
||||
// copied from 'emacs' keymap
|
||||
KM.emacsy['Alt-/'] = 'autocomplete';
|
||||
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
||||
}
|
||||
if (!extras.includes('blockComment')) {
|
||||
KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
|
||||
}
|
||||
if (UA.windows) {
|
||||
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
|
||||
if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
|
||||
if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
|
||||
if (!extras.includes('replace')) KM.pcDefault['Ctrl-R'] = 'replace';
|
||||
// try to remap non-interceptable (Shift-)Ctrl-N/T/W hotkeys
|
||||
// Note: modifier order in CodeMirror is S-C-A
|
||||
for (const char of ['N', 'T', 'W']) {
|
||||
for (const remap of [
|
||||
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
||||
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
|
||||
]) {
|
||||
const oldKey = remap.from + char;
|
||||
for (const km of Object.values(KM)) {
|
||||
const command = km[oldKey];
|
||||
if (!command) continue;
|
||||
for (const newMod of remap.to) {
|
||||
const newKey = newMod + char;
|
||||
if (newKey in km) continue;
|
||||
km[newKey] = command;
|
||||
delete km[oldKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 'basic' keymap only has basic keys by design, so we skip it
|
||||
|
||||
const extraKeysCommands = {};
|
||||
Object.keys(CodeMirror.defaults.extraKeys).forEach(key => {
|
||||
extraKeysCommands[CodeMirror.defaults.extraKeys[key]] = true;
|
||||
});
|
||||
if (!extraKeysCommands.jumpToLine) {
|
||||
CodeMirror.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
|
||||
CodeMirror.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
|
||||
CodeMirror.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
|
||||
CodeMirror.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
|
||||
}
|
||||
if (!extraKeysCommands.autocomplete) {
|
||||
// will be used by 'sublime' on PC via fallthrough
|
||||
CodeMirror.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete';
|
||||
// OSX uses Ctrl-Space and Cmd-Space for something else
|
||||
CodeMirror.keyMap.macDefault['Alt-Space'] = 'autocomplete';
|
||||
// copied from 'emacs' keymap
|
||||
CodeMirror.keyMap.emacsy['Alt-/'] = 'autocomplete';
|
||||
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
||||
}
|
||||
if (!extraKeysCommands.blockComment) {
|
||||
CodeMirror.keyMap.sublime['Shift-Ctrl-/'] = 'commentSelection';
|
||||
}
|
||||
|
||||
Object.assign(CodeMirror.prototype, {
|
||||
/**
|
||||
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
setPreprocessor(pp, force) {
|
||||
const name = pp === 'less' ? 'text/x-less' : pp === 'stylus' ? pp : 'css';
|
||||
const m = this.doc.mode;
|
||||
if (force || (m.helperType ? m.helperType !== pp : m.name !== name)) {
|
||||
this.setOption('mode', name);
|
||||
this.doc.mode.lineComment = ''; // stylelint chokes on line comments a lot
|
||||
}
|
||||
},
|
||||
/** Superfast GC-friendly check that runs until the first non-space line */
|
||||
isBlank() {
|
||||
let filled;
|
||||
this.eachLine(({text}) => (filled = text && /\S/.test(text)));
|
||||
return !filled;
|
||||
},
|
||||
/**
|
||||
* Sets cursor and centers it in view if `pos` was out of view
|
||||
* @param {CodeMirror.Pos} pos
|
||||
* @param {CodeMirror.Pos} [end] - will set a selection from `pos` to `end`
|
||||
*/
|
||||
jumpToPos(pos, end = pos) {
|
||||
const {curOp} = this;
|
||||
if (!curOp) this.startOperation();
|
||||
const y = this.cursorCoords(pos, 'window').top;
|
||||
const rect = this.display.wrapper.getBoundingClientRect();
|
||||
// case 1) outside of CM viewport or too close to edge so tell CM to render a new viewport
|
||||
if (y < rect.top + 50 || y > rect.bottom - 100) {
|
||||
this.scrollIntoView(pos, rect.height / 2);
|
||||
// case 2) inside CM viewport but outside of window viewport so just scroll the window
|
||||
} else if (y < 0 || y > innerHeight) {
|
||||
editor.scrollToEditor(this);
|
||||
}
|
||||
// Using prototype since our bookmark patch sets cm.setSelection to jumpToPos
|
||||
CodeMirror.prototype.setSelection.call(this, pos, end);
|
||||
if (!curOp) this.endOperation();
|
||||
},
|
||||
if (navigator.appVersion.includes('Windows')) {
|
||||
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
|
||||
if (!extraKeysCommands.findNext) {
|
||||
CodeMirror.keyMap.pcDefault['F3'] = 'findNext';
|
||||
}
|
||||
if (!extraKeysCommands.findPrev) {
|
||||
CodeMirror.keyMap.pcDefault['Shift-F3'] = 'findPrev';
|
||||
}
|
||||
if (!extraKeysCommands.replace) {
|
||||
CodeMirror.keyMap.pcDefault['Ctrl-R'] = 'replace';
|
||||
}
|
||||
|
||||
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
|
||||
['N', 'T', 'W'].forEach(char => {
|
||||
[
|
||||
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
||||
// Note: modifier order in CodeMirror is S-C-A
|
||||
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']}
|
||||
].forEach(remap => {
|
||||
const oldKey = remap.from + char;
|
||||
Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
|
||||
const keyMap = CodeMirror.keyMap[keyMapName];
|
||||
const command = keyMap[oldKey];
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
remap.to.some(newMod => {
|
||||
const newKey = newMod + char;
|
||||
if (!(newKey in keyMap)) {
|
||||
delete keyMap[oldKey];
|
||||
keyMap[newKey] = command;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
|
||||
// CSS Backgrounds and Borders Module L4
|
||||
'background-position-x': true,
|
||||
'background-position-y': true,
|
||||
|
||||
// CSS Logical Properties and Values L1
|
||||
'block-size': true,
|
||||
'border-block-color': true,
|
||||
'border-block-end': true,
|
||||
'border-block-end-color': true,
|
||||
'border-block-end-style': true,
|
||||
'border-block-end-width': true,
|
||||
'border-block-start': true,
|
||||
'border-block-start-color': true,
|
||||
'border-block-start-style': true,
|
||||
'border-block-start-width': true,
|
||||
'border-block-style': true,
|
||||
'border-block-width': true,
|
||||
'border-inline-color': true,
|
||||
'border-inline-end': true,
|
||||
'border-inline-end-color': true,
|
||||
'border-inline-end-style': true,
|
||||
'border-inline-end-width': true,
|
||||
'border-inline-start': true,
|
||||
'border-inline-start-color': true,
|
||||
'border-inline-start-style': true,
|
||||
'border-inline-start-width': true,
|
||||
'border-inline-style': true,
|
||||
'border-inline-width': true,
|
||||
'inline-size': true,
|
||||
'inset': true,
|
||||
'inset-block': true,
|
||||
'inset-block-end': true,
|
||||
'inset-block-start': true,
|
||||
'inset-inline': true,
|
||||
'inset-inline-end': true,
|
||||
'inset-inline-start': true,
|
||||
'margin-block': true,
|
||||
'margin-block-end': true,
|
||||
'margin-block-start': true,
|
||||
'margin-inline': true,
|
||||
'margin-inline-end': true,
|
||||
'margin-inline-start': true,
|
||||
'max-block-size': true,
|
||||
'max-inline-size': true,
|
||||
'min-block-size': true,
|
||||
'min-inline-size': true,
|
||||
'padding-block': true,
|
||||
'padding-block-end': true,
|
||||
'padding-block-start': true,
|
||||
'padding-inline': true,
|
||||
'padding-inline-end': true,
|
||||
'padding-inline-start': true,
|
||||
'text-align-all': true,
|
||||
|
||||
'contain': true,
|
||||
'mask-image': true,
|
||||
'mix-blend-mode': true,
|
||||
'rotate': true,
|
||||
'isolation': true,
|
||||
'zoom': true,
|
||||
|
||||
// https://www.w3.org/TR/css-round-display-1/
|
||||
'border-boundary': true,
|
||||
'shape': true,
|
||||
'shape-inside': true,
|
||||
'viewport-fit': true,
|
||||
|
||||
// nonstandard https://compat.spec.whatwg.org/
|
||||
'box-reflect': true,
|
||||
'text-fill-color': true,
|
||||
'text-stroke': true,
|
||||
'text-stroke-color': true,
|
||||
'text-stroke-width': true,
|
||||
// end
|
||||
});
|
||||
Object.assign(CodeMirror.mimeModes['text/css'].valueKeywords, {
|
||||
'isolate': true,
|
||||
'rect': true,
|
||||
'recto': true,
|
||||
'verso': true,
|
||||
});
|
||||
Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, {
|
||||
'darkgrey': true,
|
||||
'darkslategrey': true,
|
||||
'dimgrey': true,
|
||||
'grey': true,
|
||||
'lightgrey': true,
|
||||
'lightslategrey': true,
|
||||
'slategrey': true,
|
||||
});
|
||||
|
||||
Object.assign(CodeMirror.commands, {
|
||||
jumpToLine(cm) {
|
||||
const cur = cm.getCursor();
|
||||
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
||||
if (oldDialog) cm.focus(); // close the currently opened minidialog
|
||||
cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
|
||||
const [line, ch] = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$|$/);
|
||||
if (line) cm.setCursor(line - 1, ch ? ch - 1 : cur.ch);
|
||||
}, {value: cur.line + 1});
|
||||
const MODE = {
|
||||
less: {
|
||||
family: 'css',
|
||||
value: 'text/x-less',
|
||||
isActive: cm =>
|
||||
cm.doc.mode &&
|
||||
cm.doc.mode.name === 'css' &&
|
||||
cm.doc.mode.helperType === 'less',
|
||||
},
|
||||
stylus: 'stylus',
|
||||
uso: 'css'
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension('setPreprocessor', function (preprocessor, force = false) {
|
||||
const mode = MODE[preprocessor] || 'css';
|
||||
const isActive = mode.isActive || (
|
||||
cm => cm.doc.mode === mode ||
|
||||
cm.doc.mode && (cm.doc.mode.name + (cm.doc.mode.helperType || '') === mode)
|
||||
);
|
||||
if (!force && isActive(this)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if ((mode.family || mode) === 'css') {
|
||||
// css.js is always loaded via html
|
||||
this.setOption('mode', mode.value || mode);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return loadScript(`/vendor/codemirror/mode/${mode}/${mode}.js`).then(() => {
|
||||
this.setOption('mode', mode);
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension('isBlank', function () {
|
||||
// superfast checking as it runs only until the first non-blank line
|
||||
let isBlank = true;
|
||||
this.doc.eachLine(line => {
|
||||
if (line.text && line.text.trim()) {
|
||||
isBlank = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return isBlank;
|
||||
});
|
||||
|
||||
// editor commands
|
||||
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
|
||||
CodeMirror.commands[name] = (...args) => editor[name](...args);
|
||||
}
|
||||
|
||||
// speedup: reuse the old folding marks
|
||||
// TODO: remove when https://github.com/codemirror/CodeMirror/pull/6010 is shipped in /vendor
|
||||
const {setGutterMarker} = CodeMirror.prototype;
|
||||
CodeMirror.prototype.setGutterMarker = function (line, gutterID, value) {
|
||||
const o = this.state.foldGutter.options;
|
||||
if (typeof o.indicatorOpen === 'string' ||
|
||||
typeof o.indicatorFolded === 'string') {
|
||||
const old = line.gutterMarkers && line.gutterMarkers[gutterID];
|
||||
// old className can contain other names set by CodeMirror so we'll use classList
|
||||
if (old && value && old.classList.contains(value.className) ||
|
||||
!old && !value) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return setGutterMarker.apply(this, arguments);
|
||||
};
|
||||
|
||||
// CodeMirror convenience commands
|
||||
Object.assign(CodeMirror.commands, {
|
||||
toggleEditorFocus,
|
||||
jumpToLine,
|
||||
commentSelection,
|
||||
});
|
||||
|
||||
function jumpToLine(cm) {
|
||||
const cur = cm.getCursor();
|
||||
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
||||
if (oldDialog) {
|
||||
// close the currently opened minidialog
|
||||
cm.focus();
|
||||
}
|
||||
// make sure to focus the input in newly opened minidialog
|
||||
// setTimeout(() => {
|
||||
// $('.CodeMirror-dialog', section).focus();
|
||||
// });
|
||||
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||
if (m) {
|
||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||
}
|
||||
}, {value: cur.line + 1});
|
||||
}
|
||||
|
||||
function commentSelection(cm) {
|
||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||
}
|
||||
|
||||
function toggleEditorFocus(cm) {
|
||||
if (!cm) return;
|
||||
if (cm.hasFocus()) {
|
||||
setTimeout(() => cm.display.input.blur());
|
||||
} else {
|
||||
cm.focus();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
CodeMirror.hint && (() => {
|
||||
const USO_VAR = 'uso-variable';
|
||||
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
|
||||
const USO_INVALID_VAR = 'error ' + USO_VAR;
|
||||
const RX_IMPORTANT = /(i(m(p(o(r(t(a(nt?)?)?)?)?)?)?)?)?(?=\b|\W|$)/iy;
|
||||
const RX_VAR_KEYWORD = /(^|[^-\w\u0080-\uFFFF])var\(/iy;
|
||||
const RX_END_OF_VAR = /[\s,)]|$/g;
|
||||
|
||||
const originalHelper = CodeMirror.hint.css || (() => {});
|
||||
const helper = cm => {
|
||||
const pos = cm.getCursor();
|
||||
const {line, ch} = pos;
|
||||
const {styles, text} = cm.getLineHandle(line);
|
||||
if (!styles) return originalHelper(cm);
|
||||
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
|
||||
if (style && (style.startsWith('comment') || style.startsWith('string'))) {
|
||||
return originalHelper(cm);
|
||||
}
|
||||
|
||||
// !important
|
||||
if (text[ch - 1] === '!' && /i|\W|^$/i.test(text[ch] || '')) {
|
||||
RX_IMPORTANT.lastIndex = ch;
|
||||
return {
|
||||
list: ['important'],
|
||||
from: pos,
|
||||
to: {line, ch: ch + RX_IMPORTANT.exec(text)[0].length},
|
||||
};
|
||||
}
|
||||
|
||||
let prev = index > 2 ? styles[index - 2] : 0;
|
||||
let end = styles[index];
|
||||
|
||||
// #hex colors
|
||||
if (text[prev] === '#') {
|
||||
return {list: [], from: pos, to: pos};
|
||||
}
|
||||
|
||||
// adjust cursor position for /*[[ and ]]*/
|
||||
const adjust = text[prev] === '/' ? 4 : 0;
|
||||
prev += adjust;
|
||||
end -= adjust;
|
||||
const leftPart = text.slice(prev, ch);
|
||||
|
||||
// --css-variables
|
||||
const startsWithDoubleDash = text[prev] === '-' && text[prev + 1] === '-';
|
||||
if (startsWithDoubleDash ||
|
||||
leftPart === '(' && testAt(RX_VAR_KEYWORD, Math.max(0, prev - 4), text)) {
|
||||
// simplified regex without CSS escapes
|
||||
const RX_CSS_VAR = new RegExp(
|
||||
'(?:^|[\\s/;{])(' +
|
||||
(leftPart.startsWith('--') ? leftPart : '--') +
|
||||
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
|
||||
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
|
||||
'gm');
|
||||
const cursor = cm.getSearchCursor(RX_CSS_VAR, null, {caseFold: false, multiline: false});
|
||||
const list = new Set();
|
||||
while (cursor.findNext()) {
|
||||
list.add(cursor.pos.match[1]);
|
||||
}
|
||||
if (!startsWithDoubleDash) {
|
||||
prev++;
|
||||
}
|
||||
RX_END_OF_VAR.lastIndex = prev;
|
||||
end = RX_END_OF_VAR.exec(text).index;
|
||||
return {
|
||||
list: [...list.keys()].sort(),
|
||||
from: {line, ch: prev},
|
||||
to: {line, ch: end},
|
||||
};
|
||||
}
|
||||
|
||||
if (!editor || !style || !style.includes(USO_VAR)) {
|
||||
return originalHelper(cm);
|
||||
}
|
||||
|
||||
// USO vars in usercss mode editor
|
||||
const vars = editor.getStyle().usercssData.vars;
|
||||
const list = vars ?
|
||||
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
|
||||
return {
|
||||
list,
|
||||
from: {line, ch: prev},
|
||||
to: {line, ch: end},
|
||||
};
|
||||
};
|
||||
CodeMirror.registerHelper('hint', 'css', helper);
|
||||
CodeMirror.registerHelper('hint', 'stylus', helper);
|
||||
|
||||
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks;
|
||||
const originalCommentHook = hooks['/'];
|
||||
hooks['/'] = tokenizeUsoVariables;
|
||||
|
||||
function tokenizeUsoVariables(stream) {
|
||||
const token = originalCommentHook.apply(this, arguments);
|
||||
if (token[1] !== 'comment') {
|
||||
return token;
|
||||
}
|
||||
const {string, start, pos} = stream;
|
||||
// /*[[install-key]]*/
|
||||
// 01234 43210
|
||||
if (string[start + 2] === '[' &&
|
||||
string[start + 3] === '[' &&
|
||||
string[pos - 3] === ']' &&
|
||||
string[pos - 4] === ']') {
|
||||
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
|
||||
const name = vars && string.slice(start + 4, pos - 4);
|
||||
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
|
||||
token[0] = USO_VALID_VAR;
|
||||
} else {
|
||||
token[0] = USO_INVALID_VAR;
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function testAt(rx, index, text) {
|
||||
if (!rx) return false;
|
||||
rx.lastIndex = index;
|
||||
return rx.test(text);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,181 +1,86 @@
|
|||
/* global CodeMirror */
|
||||
/* global editor */
|
||||
/* global prefs */
|
||||
/* global rerouteHotkeys */// util.js
|
||||
/* global CodeMirror loadScript rerouteHotkeys prefs $ debounce $create */
|
||||
/* exported cmFactory */
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
All cm instances created by this module are collected so we can broadcast prefs
|
||||
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
||||
when the instance is not used anymore.
|
||||
All cm instances created by this module are collected so we can broadcast prefs
|
||||
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
||||
when the instance is not used anymore.
|
||||
*/
|
||||
const cmFactory = (() => {
|
||||
const editors = new Set();
|
||||
// used by `indentWithTabs` option
|
||||
const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab;
|
||||
const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab;
|
||||
|
||||
(() => {
|
||||
//#region Factory
|
||||
|
||||
const cms = new Set();
|
||||
let lazyOpt;
|
||||
|
||||
const cmFactory = window.cmFactory = {
|
||||
|
||||
create(place, options) {
|
||||
const cm = CodeMirror(place, options);
|
||||
cm.lastActive = 0;
|
||||
cms.add(cm);
|
||||
return cm;
|
||||
},
|
||||
|
||||
destroy(cm) {
|
||||
cms.delete(cm);
|
||||
},
|
||||
|
||||
globalSetOption(key, value) {
|
||||
CodeMirror.defaults[key] = value;
|
||||
if (cms.size > 4 && lazyOpt.names.includes(key)) {
|
||||
lazyOpt.set(key, value);
|
||||
} else {
|
||||
cms.forEach(cm => cm.setOption(key, value));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// focus and blur
|
||||
|
||||
const onCmFocus = cm => {
|
||||
rerouteHotkeys.toggle(false);
|
||||
cm.display.wrapper.classList.add('CodeMirror-active');
|
||||
cm.lastActive = Date.now();
|
||||
};
|
||||
const onCmBlur = cm => {
|
||||
setTimeout(() => {
|
||||
/* Delaying to next tick to avoid double-processing of the currently processed keyboard event
|
||||
* when it bubbles up from CodeMirror to `document` where the rerouter listens */
|
||||
rerouteHotkeys.toggle(true);
|
||||
const {wrapper} = cm.display;
|
||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||
});
|
||||
};
|
||||
CodeMirror.defineInitHook(cm => {
|
||||
cm.on('focus', onCmFocus);
|
||||
cm.on('blur', onCmBlur);
|
||||
CodeMirror.defineOption('tabSize', prefs.get('editor.tabSize'), (cm, value) => {
|
||||
cm.setOption('indentUnit', Number(value));
|
||||
});
|
||||
|
||||
// propagated preferences
|
||||
CodeMirror.defineOption('indentWithTabs', prefs.get('editor.indentWithTabs'), (cm, value) => {
|
||||
CodeMirror.commands.insertTab = value ?
|
||||
INSERT_TAB_COMMAND :
|
||||
INSERT_SOFT_TAB_COMMAND;
|
||||
});
|
||||
|
||||
const prefToCmOpt = k =>
|
||||
k.startsWith('editor.') &&
|
||||
k.slice('editor.'.length);
|
||||
const prefKeys = prefs.knownKeys.filter(k =>
|
||||
k !== 'editor.colorpicker' && // handled in colorpicker-helper.js
|
||||
k !== 'editor.arrowKeysTraverse' && // handled in sections-editor.js
|
||||
prefToCmOpt(k) in CodeMirror.defaults);
|
||||
const {insertTab, insertSoftTab} = CodeMirror.commands;
|
||||
CodeMirror.defineOption('autocompleteOnTyping', prefs.get('editor.autocompleteOnTyping'), (cm, value) => {
|
||||
const onOff = value ? 'on' : 'off';
|
||||
cm[onOff]('changes', autocompleteOnTyping);
|
||||
cm[onOff]('pick', autocompletePicked);
|
||||
});
|
||||
|
||||
for (const [key, fn] of Object.entries({
|
||||
'editor.tabSize'(cm, value) {
|
||||
cm.setOption('indentUnit', Number(value));
|
||||
},
|
||||
'editor.indentWithTabs'(cm, value) {
|
||||
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
|
||||
},
|
||||
'editor.matchHighlight'(cm, value) {
|
||||
const showToken = value === 'token' && /[#.\-\w]/;
|
||||
const opt = (showToken || value === 'selection') && {
|
||||
showToken,
|
||||
CodeMirror.defineOption('matchHighlight', prefs.get('editor.matchHighlight'), (cm, value) => {
|
||||
if (value === 'token') {
|
||||
cm.setOption('highlightSelectionMatches', {
|
||||
showToken: /[#.\-\w]/,
|
||||
annotateScrollbar: true,
|
||||
delay: 0,
|
||||
onUpdate: updateMatchHighlightCount,
|
||||
};
|
||||
cm.setOption('highlightSelectionMatches', opt || null);
|
||||
},
|
||||
'editor.selectByTokens'(cm, value) {
|
||||
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
||||
},
|
||||
})) {
|
||||
CodeMirror.defineOption(prefToCmOpt(key), prefs.get(key), fn);
|
||||
prefKeys.push(key);
|
||||
}
|
||||
|
||||
prefs.subscribe(prefKeys, (key, val) => {
|
||||
if (key === 'editor.theme') editor.updateTheme(val);
|
||||
cmFactory.globalSetOption(prefToCmOpt(key), val);
|
||||
onUpdate: updateMatchHighlightCount
|
||||
});
|
||||
} else if (value === 'selection') {
|
||||
cm.setOption('highlightSelectionMatches', {
|
||||
showToken: false,
|
||||
annotateScrollbar: true,
|
||||
onUpdate: updateMatchHighlightCount
|
||||
});
|
||||
} else {
|
||||
cm.setOption('highlightSelectionMatches', null);
|
||||
}
|
||||
});
|
||||
|
||||
// lazy propagation
|
||||
CodeMirror.defineOption('selectByTokens', prefs.get('editor.selectByTokens'), (cm, value) => {
|
||||
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
||||
});
|
||||
|
||||
lazyOpt = {
|
||||
names: ['theme', 'lineWrapping'],
|
||||
set(key, value) {
|
||||
const {observer, queue} = lazyOpt;
|
||||
for (const cm of cms) {
|
||||
let opts = queue.get(cm);
|
||||
if (!opts) queue.set(cm, opts = {});
|
||||
opts[key] = value;
|
||||
observer.observe(cm.display.wrapper);
|
||||
}
|
||||
},
|
||||
setNow({cm, data}) {
|
||||
cm.operation(() => data.forEach(kv => cm.setOption(...kv)));
|
||||
},
|
||||
onView(entries) {
|
||||
const {queue, observer} = lazyOpt;
|
||||
const delayed = [];
|
||||
for (const e of entries) {
|
||||
const r = e.isIntersecting && e.intersectionRect;
|
||||
if (!r) continue;
|
||||
const cm = e.target.CodeMirror;
|
||||
const data = Object.entries(queue.get(cm) || {});
|
||||
queue.delete(cm);
|
||||
observer.unobserve(e.target);
|
||||
if (!data.every(([key, val]) => cm.getOption(key) === val)) {
|
||||
if (r.bottom > 0 && r.top < window.innerHeight) {
|
||||
lazyOpt.setNow({cm, data});
|
||||
} else {
|
||||
delayed.push({cm, data});
|
||||
}
|
||||
prefs.subscribe(null, (key, value) => {
|
||||
const option = key.replace(/^editor\./, '');
|
||||
if (!option) {
|
||||
console.error('no "cm_option"', key);
|
||||
return;
|
||||
}
|
||||
// FIXME: this is implemented in `colorpicker-helper.js`.
|
||||
if (option === 'colorpicker') {
|
||||
return;
|
||||
}
|
||||
if (option === 'theme') {
|
||||
const themeLink = $('#cm-theme');
|
||||
// use non-localized 'default' internally
|
||||
if (value === 'default') {
|
||||
themeLink.href = '';
|
||||
} else {
|
||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
||||
if (themeLink.href !== url) {
|
||||
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||
return loadScript(url, true).then(([newThemeLink]) => {
|
||||
setOption(option, value);
|
||||
themeLink.remove();
|
||||
newThemeLink.id = 'cm-theme';
|
||||
});
|
||||
}
|
||||
}
|
||||
if (delayed.length) {
|
||||
setTimeout(() => delayed.forEach(lazyOpt.setNow));
|
||||
}
|
||||
},
|
||||
get observer() {
|
||||
if (!lazyOpt._observer) {
|
||||
// must exceed refreshOnView's 100%
|
||||
lazyOpt._observer = new IntersectionObserver(lazyOpt.onView, {rootMargin: '150%'});
|
||||
lazyOpt.queue = new WeakMap();
|
||||
}
|
||||
return lazyOpt._observer;
|
||||
},
|
||||
};
|
||||
|
||||
//#endregion
|
||||
//#region Commands
|
||||
|
||||
Object.assign(CodeMirror.commands, {
|
||||
commentSelection(cm) {
|
||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||
},
|
||||
toggleEditorFocus(cm) {
|
||||
if (!cm) return;
|
||||
if (cm.hasFocus()) {
|
||||
setTimeout(() => cm.display.input.blur());
|
||||
} else {
|
||||
cm.focus();
|
||||
}
|
||||
},
|
||||
}
|
||||
// broadcast option
|
||||
setOption(option, value);
|
||||
});
|
||||
for (const cmd of [
|
||||
'nextEditor',
|
||||
'prevEditor',
|
||||
'save',
|
||||
'toggleStyle',
|
||||
]) {
|
||||
CodeMirror.commands[cmd] = (...args) => editor[cmd](...args);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region CM option handlers
|
||||
return {create, destroy, setOption};
|
||||
|
||||
function updateMatchHighlightCount(cm, state) {
|
||||
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
||||
|
@ -238,76 +143,151 @@
|
|||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region Bookmarks
|
||||
function autocompleteOnTyping(cm, [info], debounced) {
|
||||
const lastLine = info.text[info.text.length - 1];
|
||||
if (
|
||||
cm.state.completionActive ||
|
||||
info.origin && !info.origin.includes('input') ||
|
||||
!lastLine
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (cm.state.autocompletePicked) {
|
||||
cm.state.autocompletePicked = false;
|
||||
return;
|
||||
}
|
||||
if (!debounced) {
|
||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
||||
return;
|
||||
}
|
||||
if (lastLine.match(/[-a-z!]+$/i)) {
|
||||
cm.state.autocompletePicked = false;
|
||||
cm.options.hintOptions.completeSingle = false;
|
||||
cm.execCommand('autocomplete');
|
||||
setTimeout(() => {
|
||||
cm.options.hintOptions.completeSingle = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const BM_CLS = 'gutter-bookmark';
|
||||
const BM_BRAND = 'sublimeBookmark';
|
||||
const BM_CLICKER = 'CodeMirror-linenumbers';
|
||||
const BM_DATA = Symbol('data');
|
||||
// TODO: revisit when https://github.com/codemirror/CodeMirror/issues/6716 is fixed
|
||||
const tmProto = CodeMirror.TextMarker.prototype;
|
||||
const tmProtoOvr = {};
|
||||
for (const k of ['clear', 'attachLine', 'detachLine']) {
|
||||
tmProtoOvr[k] = function (line) {
|
||||
const {cm} = this.doc;
|
||||
const withOp = !cm.curOp;
|
||||
if (withOp) cm.startOperation();
|
||||
tmProto[k].apply(this, arguments);
|
||||
cm.curOp.ownsGroup.delayedCallbacks.push(toggleMark.bind(this, this.lines[0], line));
|
||||
if (withOp) cm.endOperation();
|
||||
};
|
||||
function autocompletePicked(cm) {
|
||||
cm.state.autocompletePicked = true;
|
||||
}
|
||||
for (const name of ['prevBookmark', 'nextBookmark']) {
|
||||
const cmdFn = CodeMirror.commands[name];
|
||||
CodeMirror.commands[name] = cm => {
|
||||
cm.setSelection = cm.jumpToPos;
|
||||
cmdFn(cm);
|
||||
delete cm.setSelection;
|
||||
};
|
||||
|
||||
function destroy(cm) {
|
||||
editors.delete(cm);
|
||||
}
|
||||
CodeMirror.defineInitHook(cm => {
|
||||
cm.on('gutterClick', onGutterClick);
|
||||
cm.on('gutterContextMenu', onGutterContextMenu);
|
||||
cm.on('markerAdded', onMarkAdded);
|
||||
});
|
||||
// TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
|
||||
function onGutterClick(cm, line, name, e) {
|
||||
switch (name === BM_CLICKER && e.button) {
|
||||
case 0: {
|
||||
// main button: toggle
|
||||
const [mark] = cm.findMarks({line, ch: 0}, {line, ch: 1e9}, m => m[BM_BRAND]);
|
||||
cm.setCursor(mark ? mark.find(-1) : {line, ch: 0});
|
||||
cm.execCommand('toggleBookmark');
|
||||
|
||||
function create(init, options) {
|
||||
const cm = CodeMirror(init, options);
|
||||
cm.lastActive = 0;
|
||||
const wrapper = cm.display.wrapper;
|
||||
cm.on('blur', () => {
|
||||
rerouteHotkeys(true);
|
||||
setTimeout(() => {
|
||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||
});
|
||||
});
|
||||
cm.on('focus', () => {
|
||||
rerouteHotkeys(false);
|
||||
wrapper.classList.add('CodeMirror-active');
|
||||
cm.lastActive = Date.now();
|
||||
});
|
||||
editors.add(cm);
|
||||
return cm;
|
||||
}
|
||||
|
||||
function getLastActivated() {
|
||||
let result;
|
||||
for (const cm of editors) {
|
||||
if (!result || result.lastActive < cm.lastActive) {
|
||||
result = cm;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setOption(key, value) {
|
||||
CodeMirror.defaults[key] = value;
|
||||
if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) {
|
||||
throttleSetOption({key, value, index: 0});
|
||||
return;
|
||||
}
|
||||
for (const cm of editors) {
|
||||
cm.setOption(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
function throttleSetOption({
|
||||
key,
|
||||
value,
|
||||
index,
|
||||
timeStart = performance.now(),
|
||||
editorsCopy = [...editors],
|
||||
cmStart = getLastActivated(),
|
||||
progress,
|
||||
}) {
|
||||
if (index === 0) {
|
||||
if (!cmStart) {
|
||||
return;
|
||||
}
|
||||
cmStart.setOption(key, value);
|
||||
}
|
||||
|
||||
const THROTTLE_AFTER_MS = 100;
|
||||
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
||||
|
||||
const t0 = performance.now();
|
||||
const total = editorsCopy.length;
|
||||
while (index < total) {
|
||||
const cm = editorsCopy[index++];
|
||||
if (cm === cmStart || !editors.has(cm)) {
|
||||
continue;
|
||||
}
|
||||
cm.setOption(key, value);
|
||||
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
// middle button: select all marks
|
||||
cm.execCommand('selectBookmarks');
|
||||
break;
|
||||
}
|
||||
}
|
||||
function onGutterContextMenu(cm, line, name, e) {
|
||||
if (name === BM_CLICKER) {
|
||||
cm.execCommand(e.ctrlKey ? 'prevBookmark' : 'nextBookmark');
|
||||
e.preventDefault();
|
||||
if (index >= total) {
|
||||
$.remove(progress);
|
||||
return;
|
||||
}
|
||||
}
|
||||
function onMarkAdded(cm, mark) {
|
||||
if (mark[BM_BRAND]) {
|
||||
// CM bug workaround to keep the mark at line start when the above line is removed
|
||||
mark.inclusiveRight = true;
|
||||
Object.assign(mark, tmProtoOvr);
|
||||
toggleMark.call(mark, true, mark[BM_DATA] = mark.lines[0]);
|
||||
if (!progress &&
|
||||
index < total / 2 &&
|
||||
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
||||
let option = $('#editor.' + key);
|
||||
if (option) {
|
||||
if (option.type === 'checkbox') {
|
||||
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
||||
}
|
||||
progress = document.body.appendChild(
|
||||
$create('.set-option-progress', {targetElement: option}));
|
||||
}
|
||||
}
|
||||
}
|
||||
function toggleMark(state, line = this[BM_DATA]) {
|
||||
this.doc[state ? 'addLineClass' : 'removeLineClass'](line, 'gutter', BM_CLS);
|
||||
if (state) {
|
||||
const bms = this.doc.cm.state.sublimeBookmarks;
|
||||
if (!bms.includes(this)) bms.push(this);
|
||||
if (progress) {
|
||||
const optionBounds = progress.targetElement.getBoundingClientRect();
|
||||
const bounds = {
|
||||
top: optionBounds.top + window.scrollY + 1,
|
||||
left: optionBounds.left + window.scrollX + 1,
|
||||
width: (optionBounds.width - 2) * index / total | 0,
|
||||
height: optionBounds.height - 2,
|
||||
};
|
||||
const style = progress.style;
|
||||
for (const prop in bounds) {
|
||||
if (bounds[prop] !== parseFloat(style[prop])) {
|
||||
style[prop] = bounds[prop] + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(throttleSetOption, 0, {
|
||||
key,
|
||||
value,
|
||||
index,
|
||||
timeStart,
|
||||
cmStart,
|
||||
editorsCopy,
|
||||
progress,
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
})();
|
||||
|
|
File diff suppressed because one or more lines are too long
115
edit/colorpicker-helper.js
Normal file
115
edit/colorpicker-helper.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
/* global CodeMirror showHelp cmFactory onDOMready $ $create prefs t */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
onDOMready().then(() => {
|
||||
$('#colorpicker-settings').onclick = configureColorpicker;
|
||||
});
|
||||
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
|
||||
prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
|
||||
setColorpickerOption(null, prefs.get('editor.colorpicker'));
|
||||
|
||||
function setColorpickerOption(id, enabled) {
|
||||
const defaults = CodeMirror.defaults;
|
||||
const keyName = prefs.get('editor.colorpicker.hotkey');
|
||||
defaults.colorpicker = enabled;
|
||||
if (enabled) {
|
||||
if (keyName) {
|
||||
CodeMirror.commands.colorpicker = invokeColorpicker;
|
||||
defaults.extraKeys = defaults.extraKeys || {};
|
||||
defaults.extraKeys[keyName] = 'colorpicker';
|
||||
}
|
||||
defaults.colorpicker = {
|
||||
// FIXME: who uses this?
|
||||
// forceUpdate: editor.getEditors().length > 0,
|
||||
tooltip: t('colorpickerTooltip'),
|
||||
popup: {
|
||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
|
||||
hideDelay: 5000,
|
||||
embedderCallback: state => {
|
||||
['hexUppercase', 'color']
|
||||
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
|
||||
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (defaults.extraKeys) {
|
||||
delete defaults.extraKeys[keyName];
|
||||
}
|
||||
}
|
||||
cmFactory.setOption('colorpicker', defaults.colorpicker);
|
||||
}
|
||||
|
||||
function registerHotkey(id, hotkey) {
|
||||
CodeMirror.commands.colorpicker = invokeColorpicker;
|
||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||
for (const key in extraKeys) {
|
||||
if (extraKeys[key] === 'colorpicker') {
|
||||
delete extraKeys[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hotkey) {
|
||||
extraKeys[hotkey] = 'colorpicker';
|
||||
}
|
||||
}
|
||||
|
||||
function invokeColorpicker(cm) {
|
||||
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
|
||||
}
|
||||
|
||||
function configureColorpicker(event) {
|
||||
event.preventDefault();
|
||||
const input = $create('input', {
|
||||
type: 'search',
|
||||
spellcheck: false,
|
||||
value: prefs.get('editor.colorpicker.hotkey'),
|
||||
onkeydown(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const key = CodeMirror.keyName(event);
|
||||
switch (key) {
|
||||
case 'Enter':
|
||||
if (this.checkValidity()) {
|
||||
$('#help-popup .dismiss').onclick();
|
||||
}
|
||||
return;
|
||||
case 'Esc':
|
||||
$('#help-popup .dismiss').onclick();
|
||||
return;
|
||||
default:
|
||||
// disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys
|
||||
if (!key || new RegExp('^(' + [
|
||||
'(Back)?Space',
|
||||
'(Shift-)?.', // a single character
|
||||
'(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)',
|
||||
].join('|') + ')$', 'i').test(key)) {
|
||||
this.value = key || this.value;
|
||||
this.setCustomValidity('Not allowed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.value = key;
|
||||
this.setCustomValidity('');
|
||||
prefs.set('editor.colorpicker.hotkey', key);
|
||||
},
|
||||
oninput() {
|
||||
// fired on pressing "x" to clear the field
|
||||
prefs.set('editor.colorpicker.hotkey', '');
|
||||
},
|
||||
onpaste(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const popup = showHelp(t('helpKeyMapHotkey'), input);
|
||||
if (this instanceof Element) {
|
||||
const bounds = this.getBoundingClientRect();
|
||||
popup.style.left = bounds.right + 10 + 'px';
|
||||
popup.style.top = bounds.top - popup.clientHeight / 2 + 'px';
|
||||
popup.style.right = 'auto';
|
||||
}
|
||||
input.focus();
|
||||
}
|
||||
})();
|
|
@ -1,68 +0,0 @@
|
|||
/* global messageBoxProxy */// dom.js
|
||||
/* global API */// msg.js
|
||||
/* global clamp debounce */// toolbox.js
|
||||
/* global editor */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
(async function AutosaveDrafts() {
|
||||
const makeId = () => editor.style.id || 'new';
|
||||
let delay;
|
||||
let port;
|
||||
connectPort();
|
||||
|
||||
const draft = await API.drafts.get(makeId());
|
||||
if (draft && draft.isUsercss === editor.isUsercss) {
|
||||
const date = makeRelativeDate(draft.date);
|
||||
if (await messageBoxProxy.confirm(t('draftAction'), 'danger', t('draftTitle', date))) {
|
||||
await editor.replaceStyle(draft.style, draft);
|
||||
} else {
|
||||
API.drafts.delete(makeId());
|
||||
}
|
||||
}
|
||||
|
||||
editor.dirty.onChange(isDirty => isDirty ? connectPort() : port.disconnect());
|
||||
editor.dirty.onDataChange(isDirty => debounce(updateDraft, isDirty ? delay : 0));
|
||||
|
||||
prefs.subscribe('editor.autosaveDraft', (key, val) => {
|
||||
delay = clamp(val * 1000 | 0, 1000, 2 ** 32 - 1);
|
||||
const t = debounce.timers.get(updateDraft);
|
||||
if (t != null) debounce(updateDraft, t ? delay : 0);
|
||||
}, {runNow: true});
|
||||
|
||||
function connectPort() {
|
||||
port = chrome.runtime.connect({name: 'draft:' + makeId()});
|
||||
}
|
||||
|
||||
function makeRelativeDate(date) {
|
||||
let delta = (Date.now() - date) / 1000;
|
||||
if (delta >= 0 && Intl.RelativeTimeFormat) {
|
||||
for (const [span, unit, frac = 1] of [
|
||||
[60, 'second', 0],
|
||||
[60, 'minute', 0],
|
||||
[24, 'hour'],
|
||||
[7, 'day'],
|
||||
[4, 'week'],
|
||||
[12, 'month'],
|
||||
[1e99, 'year'],
|
||||
]) {
|
||||
if (delta < span) {
|
||||
return new Intl.RelativeTimeFormat({style: 'short'}).format(-delta.toFixed(frac), unit);
|
||||
}
|
||||
delta /= span;
|
||||
}
|
||||
}
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function updateDraft(isDirty = editor.dirty.isDirty()) {
|
||||
if (!isDirty) return;
|
||||
API.drafts.put({
|
||||
date: Date.now(),
|
||||
isUsercss: editor.isUsercss,
|
||||
style: editor.getValue(true),
|
||||
si: editor.makeScrollInfo(),
|
||||
}, makeId());
|
||||
}
|
||||
})();
|
806
edit/edit.css
806
edit/edit.css
File diff suppressed because it is too large
Load Diff
878
edit/edit.js
878
edit/edit.js
|
@ -1,174 +1,327 @@
|
|||
/* global $$ $ $create */// dom.js
|
||||
/* global API msg */// msg.js
|
||||
/* global CodeMirror */
|
||||
/* global SectionsEditor */
|
||||
/* global SourceEditor */
|
||||
/* global clipString createHotkeyInput helpPopup */// util.js
|
||||
/* global closeCurrentTab deepEqual mapObj sessionStore tryJSONparse */// toolbox.js
|
||||
/* global cmFactory */
|
||||
/* global editor EditorHeader */// base.js
|
||||
/* global linterMan */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
||||
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
|
||||
closeCurrentTab messageBox debounce workerUtil
|
||||
beautify ignoreChromeError
|
||||
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
|
||||
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
||||
'use strict';
|
||||
|
||||
//#region init
|
||||
|
||||
document.body.appendChild(t.template.body);
|
||||
|
||||
EditorMethods();
|
||||
editor.styleReady.then(async () => {
|
||||
EditorHeader();
|
||||
dispatchEvent(new Event('domReady'));
|
||||
await (editor.isUsercss ? SourceEditor : SectionsEditor)();
|
||||
|
||||
editor.dirty.onChange(editor.updateDirty);
|
||||
prefs.subscribe('editor.linter', () => linterMan.run());
|
||||
|
||||
// enabling after init to prevent flash of validation failure on an empty name
|
||||
$('#name').required = !editor.isUsercss;
|
||||
$('#save-button').onclick = editor.save;
|
||||
$('#cancel-button').onclick = editor.cancel;
|
||||
|
||||
const elSec = $('#sections-list');
|
||||
// editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work
|
||||
if (elSec.open) editor.updateToc();
|
||||
// and we also toggle `open` directly in other places e.g. in detectLayout()
|
||||
new MutationObserver(() => elSec.open && editor.updateToc())
|
||||
.observe(elSec, {attributes: true, attributeFilter: ['open']});
|
||||
|
||||
$('#toc').onclick = e =>
|
||||
editor.jumpToEditor([...$('#toc').children].indexOf(e.target));
|
||||
$('#keyMap-help').onclick = () =>
|
||||
require(['/edit/show-keymap-help'], () => showKeymapHelp()); /* global showKeymapHelp */
|
||||
$('#linter-settings').onclick = () =>
|
||||
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
|
||||
$('#lint-help').onclick = () =>
|
||||
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
|
||||
$('#style-settings-btn').onclick = () => require([
|
||||
'/edit/settings.css',
|
||||
'/edit/settings', /* global StyleSettings */
|
||||
], () => StyleSettings());
|
||||
|
||||
require([
|
||||
'/edit/autocomplete',
|
||||
'/edit/drafts',
|
||||
'/edit/global-search',
|
||||
]);
|
||||
const editorWorker = workerUtil.createWorker({
|
||||
url: '/edit/editor-worker.js'
|
||||
});
|
||||
|
||||
editor.styleReady.then(async () => {
|
||||
// Set up mini-header on scroll
|
||||
const {isUsercss} = editor;
|
||||
const el = $create({
|
||||
style: `
|
||||
top: 0;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
`.replace(/;/g, '!important;'),
|
||||
let saveSizeOnClose;
|
||||
|
||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||
const CssToProperty = Object.entries(propertyToCss)
|
||||
.reduce((o, v) => {
|
||||
o[v[1]] = v[0];
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
let editor;
|
||||
|
||||
let scrollPointTimer;
|
||||
|
||||
document.addEventListener('visibilitychange', beforeUnload);
|
||||
window.addEventListener('beforeunload', beforeUnload);
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
preinit();
|
||||
|
||||
(() => {
|
||||
onDOMready().then(() => {
|
||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||
showHotkeyInTooltip();
|
||||
|
||||
buildThemeElement();
|
||||
buildKeymapElement();
|
||||
|
||||
setupLivePrefs();
|
||||
});
|
||||
const scroller = isUsercss ? $('.CodeMirror-scroll') : document.body;
|
||||
const xoRoot = isUsercss ? scroller : undefined;
|
||||
const xo = new IntersectionObserver(onScrolled, {root: xoRoot});
|
||||
scroller.appendChild(el);
|
||||
onCompactToggled(editor.mqCompact);
|
||||
editor.mqCompact.on('change', onCompactToggled);
|
||||
|
||||
/** @param {MediaQueryList} mq */
|
||||
function onCompactToggled(mq) {
|
||||
for (const el of $$('details[data-pref]')) {
|
||||
el.open = mq.matches ? false : prefs.get(el.dataset.pref);
|
||||
initEditor();
|
||||
|
||||
function getCodeMirrorThemes() {
|
||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme'),
|
||||
...CODEMIRROR_THEMES
|
||||
];
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
return Promise.resolve(themes);
|
||||
}
|
||||
if (mq.matches) {
|
||||
xo.observe(el);
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
||||
themeDir.createReader().readEntries(entries => {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme')
|
||||
].concat(
|
||||
entries.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||
);
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
resolve(themes);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function findKeyForCommand(command, map) {
|
||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||
let key = Object.keys(map).find(k => map[k] === command);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
||||
key = ft && findKeyForCommand(command, ft);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildThemeElement() {
|
||||
const themeElement = $('#editor.theme');
|
||||
const themeList = localStorage.codeMirrorThemes;
|
||||
|
||||
const optionsFromArray = options => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
||||
themeElement.appendChild(fragment);
|
||||
};
|
||||
|
||||
if (themeList) {
|
||||
optionsFromArray(themeList.split(/\s+/));
|
||||
} else {
|
||||
xo.disconnect();
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
const theme = prefs.get('editor.theme');
|
||||
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
||||
getCodeMirrorThemes().then(() => {
|
||||
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||
optionsFromArray(themes);
|
||||
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
}
|
||||
}
|
||||
/** @param {IntersectionObserverEntry[]} entries */
|
||||
function onScrolled(entries) {
|
||||
const h = $('#header');
|
||||
const sticky = !entries.pop().isIntersecting;
|
||||
if (!isUsercss) scroller.style.paddingTop = sticky ? h.offsetHeight + 'px' : '';
|
||||
h.classList.toggle('sticky', sticky);
|
||||
|
||||
function buildKeymapElement() {
|
||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
||||
const maps = Object.keys(CodeMirror.keyMap)
|
||||
.map(name => ({
|
||||
value: name,
|
||||
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
||||
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
||||
}))
|
||||
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let bin = fragment;
|
||||
let groupName;
|
||||
// group suffixed maps in <optgroup>
|
||||
maps.forEach(({value, name}, i) => {
|
||||
groupName = !name.includes('-') ? name : groupName;
|
||||
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
||||
if (groupWithNext) {
|
||||
if (bin === fragment) {
|
||||
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
||||
}
|
||||
}
|
||||
const el = bin.appendChild($create('option', {value}, name));
|
||||
if (value === prefs.defaults['editor.keyMap']) {
|
||||
el.dataset.default = '';
|
||||
el.title = t('defaultTheme');
|
||||
}
|
||||
if (!groupWithNext) bin = fragment;
|
||||
});
|
||||
$('#editor.keyMap').appendChild(fragment);
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region events
|
||||
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||
for (const el of $$('[data-hotkey-tooltip]')) {
|
||||
if (el._hotkeyTooltipKeyMap !== mapName) {
|
||||
el._hotkeyTooltipKeyMap = mapName;
|
||||
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
||||
const cmd = el.dataset.hotkeyTooltip;
|
||||
const key = cmd[0] === '=' ? cmd.slice(1) :
|
||||
findKeyForCommand(cmd, mapName) ||
|
||||
extraKeys && findKeyForCommand(cmd, extraKeys);
|
||||
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
||||
if (el.title !== newTitle) el.title = newTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg.onExtension(request => {
|
||||
const {style} = request;
|
||||
function initEditor() {
|
||||
return Promise.all([
|
||||
initStyleData(),
|
||||
onDOMready(),
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(([style]) => {
|
||||
const usercss = isUsercss(style);
|
||||
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
||||
|
||||
$('#preview-label').classList.toggle('hidden', !style.id);
|
||||
|
||||
$('#beautify').onclick = () => beautify(editor.getEditors());
|
||||
window.addEventListener('resize', () => {
|
||||
debounce(rememberWindowSize, 100);
|
||||
detectLayout();
|
||||
});
|
||||
detectLayout();
|
||||
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
||||
style,
|
||||
onTitleChanged: updateTitle
|
||||
});
|
||||
editor.dirty.onChange(updateDirty);
|
||||
return Promise.resolve(editor.ready && editor.ready())
|
||||
.then(updateDirty);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
if (editor) {
|
||||
const styleName = editor.getStyle().name;
|
||||
const isDirty = editor.dirty.isDirty();
|
||||
document.title = (isDirty ? '* ' : '') + styleName;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDirty() {
|
||||
const isDirty = editor.dirty.isDirty();
|
||||
document.body.classList.toggle('dirty', isDirty);
|
||||
$('#save-button').disabled = !isDirty;
|
||||
updateTitle();
|
||||
}
|
||||
})();
|
||||
|
||||
function preinit() {
|
||||
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
||||
new MutationObserver((mutations, observer) => {
|
||||
const themeElement = $('#cm-theme');
|
||||
if (themeElement) {
|
||||
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
|
||||
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
|
||||
observer.disconnect();
|
||||
}
|
||||
}).observe(document, {subtree: true, childList: true});
|
||||
|
||||
if (chrome.windows) {
|
||||
queryTabs({currentWindow: true}).then(tabs => {
|
||||
const windowId = tabs[0].windowId;
|
||||
if (prefs.get('openEditInWindow')) {
|
||||
if (
|
||||
/true/.test(sessionStorage.saveSizeOnClose) &&
|
||||
'left' in prefs.get('windowPosition', {}) &&
|
||||
!isWindowMaximized()
|
||||
) {
|
||||
// window was reopened via Ctrl-Shift-T etc.
|
||||
chrome.windows.update(windowId, prefs.get('windowPosition'));
|
||||
}
|
||||
if (tabs.length === 1 && window.history.length === 1) {
|
||||
chrome.windows.getAll(windows => {
|
||||
if (windows.length > 1) {
|
||||
sessionStorageHash('saveSizeOnClose').set(windowId, true);
|
||||
saveSizeOnClose = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOwnTab().then(tab => {
|
||||
const ownTabId = tab.id;
|
||||
|
||||
// use browser history back when 'back to manage' is clicked
|
||||
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
||||
onDOMready().then(() => {
|
||||
$('#cancel-button').onclick = event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
history.back();
|
||||
};
|
||||
});
|
||||
}
|
||||
// no windows on android
|
||||
if (!chrome.windows) {
|
||||
return;
|
||||
}
|
||||
// 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.onAttached.addListener((tabId, info) => {
|
||||
if (tabId !== ownTabId) {
|
||||
return;
|
||||
}
|
||||
if (info.newPosition !== 0) {
|
||||
prefs.set('openEditInWindow', false);
|
||||
return;
|
||||
}
|
||||
chrome.windows.get(info.newWindowId, {populate: true}, win => {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
const openEditInWindow = win.tabs.length === 1;
|
||||
if (openEditInWindow && FIREFOX) {
|
||||
// FF-only because Chrome retardedly resets the size during dragging
|
||||
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
||||
}
|
||||
prefs.set('openEditInWindow', openEditInWindow);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onRuntimeMessage(request) {
|
||||
switch (request.method) {
|
||||
case 'styleUpdated':
|
||||
if (editor.style.id === style.id) {
|
||||
handleExternalUpdate(request);
|
||||
if (
|
||||
editor.getStyleId() === request.style.id &&
|
||||
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
|
||||
.includes(request.reason)
|
||||
) {
|
||||
Promise.resolve(
|
||||
request.codeIsUpdated === false ?
|
||||
request.style : API.getStyle(request.style.id)
|
||||
)
|
||||
.then(newStyle => {
|
||||
editor.replaceStyle(newStyle, request.codeIsUpdated);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
if (editor.style.id === style.id) {
|
||||
if (editor.getStyleId() === request.style.id) {
|
||||
document.removeEventListener('visibilitychange', beforeUnload);
|
||||
document.removeEventListener('beforeunload', beforeUnload);
|
||||
closeCurrentTab();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'editDeleteText':
|
||||
document.execCommand('delete');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleExternalUpdate({style, reason}) {
|
||||
if (reason === 'editPreview' ||
|
||||
reason === 'editPreviewEnd') {
|
||||
return;
|
||||
}
|
||||
if (reason === 'editSave' && editor.saving) {
|
||||
editor.saving = false;
|
||||
return;
|
||||
}
|
||||
if (reason === 'toggle') {
|
||||
if (editor.dirty.isDirty()) {
|
||||
editor.toggleStyle(style.enabled);
|
||||
} else {
|
||||
Object.assign(editor.style, style);
|
||||
}
|
||||
editor.updateMeta();
|
||||
editor.updateLivePreview();
|
||||
return;
|
||||
}
|
||||
style = await API.styles.get(style.id);
|
||||
if (reason === 'config') {
|
||||
delete style.sourceCode;
|
||||
delete style.sections;
|
||||
delete style.name;
|
||||
delete style.enabled;
|
||||
Object.assign(editor.style, style);
|
||||
} else {
|
||||
await editor.replaceStyle(style);
|
||||
}
|
||||
window.dispatchEvent(new Event('styleSettings'));
|
||||
}
|
||||
|
||||
window.on('beforeunload', e => {
|
||||
let pos;
|
||||
if (editor.isWindowed &&
|
||||
document.visibilityState === 'visible' &&
|
||||
prefs.get('openEditInWindow') &&
|
||||
screenX !== -32000 && // Chrome uses this value for minimized windows
|
||||
( // only if not maximized
|
||||
screenX > 0 || outerWidth < screen.availWidth ||
|
||||
screenY > 0 || outerHeight < screen.availHeight ||
|
||||
screenX <= -10 || outerWidth >= screen.availWidth + 10 ||
|
||||
screenY <= -10 || outerHeight >= screen.availHeight + 10
|
||||
)
|
||||
) {
|
||||
pos = {
|
||||
left: screenX,
|
||||
top: screenY,
|
||||
width: outerWidth,
|
||||
height: outerHeight,
|
||||
};
|
||||
prefs.set('windowPosition', pos);
|
||||
}
|
||||
sessionStore.windowPos = JSON.stringify(pos || {});
|
||||
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify(editor.makeScrollInfo());
|
||||
/**
|
||||
* Invoked for 'visibilitychange' event by default.
|
||||
* Invoked for 'beforeunload' event when the style is modified and unsaved.
|
||||
* See https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid
|
||||
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal.
|
||||
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved.
|
||||
*/
|
||||
function beforeUnload(e) {
|
||||
if (saveSizeOnClose) rememberWindowSize();
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
// blurring triggers 'change' or 'input' event if needed
|
||||
|
@ -176,216 +329,255 @@ window.on('beforeunload', e => {
|
|||
// refocus if unloading was canceled
|
||||
setTimeout(() => activeElement.focus());
|
||||
}
|
||||
if (editor.dirty.isDirty()) {
|
||||
if (editor && editor.dirty.isDirty()) {
|
||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||
e.returnValue = t('styleChangesNotSaved');
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region editor methods
|
||||
|
||||
function EditorMethods() {
|
||||
const toc = [];
|
||||
const {dirty} = editor;
|
||||
let {style} = editor;
|
||||
let wasDirty = false;
|
||||
|
||||
Object.defineProperties(editor, {
|
||||
scrollInfo: {
|
||||
get: () => style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]) || {},
|
||||
},
|
||||
style: {
|
||||
get: () => style,
|
||||
set: val => (style = val),
|
||||
},
|
||||
});
|
||||
|
||||
/** @namespace Editor */
|
||||
Object.assign(editor, {
|
||||
|
||||
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
|
||||
if (si && si.sel) {
|
||||
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
|
||||
cm.setSelections(...si.sel, {scroll: false});
|
||||
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
|
||||
Object.assign(cm.display.scroller, si.scroll); // for source editor
|
||||
Object.assign(cm.doc, si.scroll); // for sectioned editor
|
||||
}
|
||||
},
|
||||
|
||||
makeScrollInfo() {
|
||||
return {
|
||||
scrollY: window.scrollY,
|
||||
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
|
||||
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
|
||||
focus: cm.hasFocus(),
|
||||
height: cm.display.wrapper.style.height.replace('100vh', ''),
|
||||
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
|
||||
scroll: mapObj(cm.doc, null, ['scrollLeft', 'scrollTop']),
|
||||
sel: [cm.doc.sel.ranges, cm.doc.sel.primIndex],
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (dirty.isDirty()) {
|
||||
editor.saving = true;
|
||||
await editor.saveImpl();
|
||||
}
|
||||
},
|
||||
|
||||
toggleStyle(enabled = !style.enabled) {
|
||||
$('#enabled').checked = enabled;
|
||||
editor.updateEnabledness(enabled);
|
||||
},
|
||||
|
||||
updateDirty() {
|
||||
const isDirty = dirty.isDirty();
|
||||
if (wasDirty !== isDirty) {
|
||||
wasDirty = isDirty;
|
||||
document.body.classList.toggle('dirty', isDirty);
|
||||
$('#save-button').disabled = !isDirty;
|
||||
}
|
||||
editor.updateTitle();
|
||||
},
|
||||
|
||||
updateEnabledness(enabled) {
|
||||
dirty.modify('enabled', style.enabled, enabled);
|
||||
style.enabled = enabled;
|
||||
editor.updateLivePreview();
|
||||
},
|
||||
|
||||
updateName(isUserInput) {
|
||||
if (!editor) return;
|
||||
if (isUserInput) {
|
||||
const {value} = $('#name');
|
||||
dirty.modify('name', style[editor.nameTarget] || style.name, value);
|
||||
style[editor.nameTarget] = value;
|
||||
}
|
||||
editor.updateTitle();
|
||||
},
|
||||
|
||||
updateToc(added = editor.sections) {
|
||||
if (!toc.el) {
|
||||
toc.el = $('#toc');
|
||||
toc.elDetails = toc.el.closest('details');
|
||||
}
|
||||
if (!toc.elDetails.open) return;
|
||||
const {sections} = editor;
|
||||
const first = sections.indexOf(added[0]);
|
||||
const elFirst = toc.el.children[first];
|
||||
if (first >= 0 && (!added.focus || !elFirst)) {
|
||||
for (let el = elFirst, i = first; i < sections.length; i++) {
|
||||
const entry = sections[i].tocEntry;
|
||||
if (!deepEqual(entry, toc[i])) {
|
||||
if (!el) el = toc.el.appendChild($create('li', {tabIndex: 0}));
|
||||
el.tabIndex = entry.removed ? -1 : 0;
|
||||
toc[i] = Object.assign({}, entry);
|
||||
const s = el.textContent = clipString(entry.label) || (
|
||||
entry.target == null
|
||||
? t('appliesToEverything')
|
||||
: clipString(entry.target) + (entry.numTargets > 1 ? ', ...' : ''));
|
||||
if (s.length > 30) el.title = s;
|
||||
}
|
||||
el = el.nextElementSibling;
|
||||
}
|
||||
}
|
||||
while (toc.length > sections.length) {
|
||||
toc.el.lastElementChild.remove();
|
||||
toc.length--;
|
||||
}
|
||||
if (added.focus) {
|
||||
const cls = 'current';
|
||||
const old = $('.' + cls, toc.el);
|
||||
const el = elFirst || toc.el.children[first];
|
||||
if (old && old !== el) old.classList.remove(cls);
|
||||
el.classList.add(cls);
|
||||
}
|
||||
},
|
||||
|
||||
useSavedStyle(newStyle) {
|
||||
if (style.id !== newStyle.id) {
|
||||
history.replaceState({}, '', `?id=${newStyle.id}`);
|
||||
}
|
||||
sessionStore.justEditedStyleId = newStyle.id;
|
||||
Object.assign(style, newStyle);
|
||||
editor.updateClass();
|
||||
editor.updateMeta();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region colorpickerHelper
|
||||
function isUsercss(style) {
|
||||
return (
|
||||
style.usercssData ||
|
||||
!style.id && prefs.get('newStyleAsUsercss')
|
||||
);
|
||||
}
|
||||
|
||||
(async function colorpickerHelper() {
|
||||
prefs.subscribe('editor.colorpicker.hotkey', (id, hotkey) => {
|
||||
CodeMirror.commands.colorpicker = invokeColorpicker;
|
||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||
for (const key in extraKeys) {
|
||||
if (extraKeys[key] === 'colorpicker') {
|
||||
delete extraKeys[key];
|
||||
break;
|
||||
function initStyleData() {
|
||||
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||
const id = Number(params.get('id'));
|
||||
const createEmptyStyle = () => ({
|
||||
name: params.get('domain') ||
|
||||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
||||
'',
|
||||
enabled: true,
|
||||
sections: [
|
||||
Object.assign({code: ''},
|
||||
...Object.keys(CssToProperty)
|
||||
.map(name => ({
|
||||
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
|
||||
}))
|
||||
)
|
||||
],
|
||||
});
|
||||
return fetchStyle()
|
||||
.then(style => {
|
||||
if (style.id) sessionStorage.justEditedStyleId = style.id;
|
||||
// we set "usercss" class on <html> when <body> is empty
|
||||
// so there'll be no flickering of the elements that depend on it
|
||||
if (isUsercss(style)) {
|
||||
document.documentElement.classList.add('usercss');
|
||||
}
|
||||
// strip URL parameters when invoked for a non-existent id
|
||||
if (!style.id) {
|
||||
history.replaceState({}, document.title, location.pathname);
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
function fetchStyle() {
|
||||
if (id) {
|
||||
return API.getStyle(id);
|
||||
}
|
||||
if (hotkey) {
|
||||
extraKeys[hotkey] = 'colorpicker';
|
||||
return Promise.resolve(createEmptyStyle());
|
||||
}
|
||||
}
|
||||
|
||||
function showHelp(title = '', body) {
|
||||
const div = $('#help-popup');
|
||||
div.className = '';
|
||||
|
||||
const contents = $('.contents', div);
|
||||
contents.textContent = '';
|
||||
if (body) {
|
||||
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
|
||||
}
|
||||
|
||||
$('.title', div).textContent = title;
|
||||
|
||||
showHelp.close = showHelp.close || (event => {
|
||||
const canClose =
|
||||
!event ||
|
||||
event.type === 'click' ||
|
||||
(
|
||||
event.which === 27 &&
|
||||
!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
|
||||
!$('.CodeMirror-hints, #message-box') &&
|
||||
(
|
||||
!document.activeElement ||
|
||||
!document.activeElement.closest('#search-replace-dialog') &&
|
||||
document.activeElement.matches(':not(input), .can-close-on-esc')
|
||||
)
|
||||
);
|
||||
if (!canClose) {
|
||||
return;
|
||||
}
|
||||
if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
|
||||
setTimeout(() => {
|
||||
messageBox.confirm(t('confirmDiscardChanges'))
|
||||
.then(ok => ok && showHelp.close());
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (div.contains(document.activeElement) && showHelp.originalFocus) {
|
||||
showHelp.originalFocus.focus();
|
||||
}
|
||||
div.style.display = '';
|
||||
contents.textContent = '';
|
||||
clearTimeout(contents.timer);
|
||||
window.removeEventListener('keydown', showHelp.close, true);
|
||||
window.dispatchEvent(new Event('closeHelp'));
|
||||
});
|
||||
|
||||
prefs.subscribe('editor.colorpicker', (id, enabled) => {
|
||||
const defaults = CodeMirror.defaults;
|
||||
const keyName = prefs.get('editor.colorpicker.hotkey');
|
||||
defaults.colorpicker = enabled;
|
||||
if (enabled) {
|
||||
if (keyName) {
|
||||
CodeMirror.commands.colorpicker = invokeColorpicker;
|
||||
defaults.extraKeys = defaults.extraKeys || {};
|
||||
defaults.extraKeys[keyName] = 'colorpicker';
|
||||
window.addEventListener('keydown', showHelp.close, true);
|
||||
$('.dismiss', div).onclick = showHelp.close;
|
||||
|
||||
// reset any inline styles
|
||||
div.style = 'display: block';
|
||||
|
||||
showHelp.originalFocus = document.activeElement;
|
||||
return div;
|
||||
}
|
||||
|
||||
function showCodeMirrorPopup(title, html, options) {
|
||||
const popup = showHelp(title, html);
|
||||
popup.classList.add('big');
|
||||
|
||||
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
|
||||
mode: 'css',
|
||||
lineNumbers: true,
|
||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
matchBrackets: true,
|
||||
styleActiveLine: true,
|
||||
theme: prefs.get('editor.theme'),
|
||||
keyMap: prefs.get('editor.keyMap')
|
||||
}, options));
|
||||
cm.focus();
|
||||
rerouteHotkeys(false);
|
||||
|
||||
document.documentElement.style.pointerEvents = 'none';
|
||||
popup.style.pointerEvents = 'auto';
|
||||
|
||||
const onKeyDown = event => {
|
||||
if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
const search = $('#search-replace-dialog');
|
||||
const area = search && search.contains(document.activeElement) ? search : popup;
|
||||
moveFocus(area, event.shiftKey ? -1 : 1);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown, true);
|
||||
|
||||
window.addEventListener('closeHelp', function _() {
|
||||
window.removeEventListener('closeHelp', _);
|
||||
window.removeEventListener('keydown', onKeyDown, true);
|
||||
document.documentElement.style.removeProperty('pointer-events');
|
||||
rerouteHotkeys(true);
|
||||
cm = popup.codebox = null;
|
||||
});
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
function rememberWindowSize() {
|
||||
if (
|
||||
document.visibilityState === 'visible' &&
|
||||
prefs.get('openEditInWindow') &&
|
||||
!isWindowMaximized()
|
||||
) {
|
||||
prefs.set('windowPosition', {
|
||||
left: window.screenX,
|
||||
top: window.screenY,
|
||||
width: window.outerWidth,
|
||||
height: window.outerHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
prefs.subscribe(['editor.linter'], (key, value) => {
|
||||
$('body').classList.toggle('linter-disabled', value === '');
|
||||
});
|
||||
|
||||
function fixedHeader() {
|
||||
const scrollPoint = $('#header').clientHeight - 40;
|
||||
const linterEnabled = prefs.get('editor.linter') !== '';
|
||||
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
|
||||
$('body').classList.add('fixed-header');
|
||||
} else if (window.scrollY < 40 && linterEnabled) {
|
||||
$('body').classList.remove('fixed-header');
|
||||
}
|
||||
}
|
||||
|
||||
function detectLayout() {
|
||||
const body = $('body');
|
||||
const options = $('#options');
|
||||
const lint = $('#lint');
|
||||
const compact = window.innerWidth <= 850;
|
||||
const shortViewportLinter = window.innerHeight < 692;
|
||||
const shortViewportNoLinter = window.innerHeight < 554;
|
||||
const linterEnabled = prefs.get('editor.linter') !== '';
|
||||
if (compact) {
|
||||
body.classList.add('compact-layout');
|
||||
options.removeAttribute('open');
|
||||
options.classList.add('ignore-pref');
|
||||
lint.removeAttribute('open');
|
||||
lint.classList.add('ignore-pref');
|
||||
if (!$('.usercss')) {
|
||||
clearTimeout(scrollPointTimer);
|
||||
scrollPointTimer = setTimeout(() => {
|
||||
const scrollPoint = $('#header').clientHeight - 40;
|
||||
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
|
||||
body.classList.add('fixed-header');
|
||||
}
|
||||
}, 250);
|
||||
window.addEventListener('scroll', fixedHeader);
|
||||
}
|
||||
} else {
|
||||
body.classList.remove('compact-layout');
|
||||
body.classList.remove('fixed-header');
|
||||
window.removeEventListener('scroll', fixedHeader);
|
||||
if (shortViewportLinter && linterEnabled || shortViewportNoLinter && !linterEnabled) {
|
||||
options.removeAttribute('open');
|
||||
options.classList.add('ignore-pref');
|
||||
if (prefs.get('editor.lint.expanded')) {
|
||||
lint.setAttribute('open', '');
|
||||
}
|
||||
defaults.colorpicker = {
|
||||
tooltip: t('colorpickerTooltip'),
|
||||
popup: {
|
||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||
paletteLine: t('numberedLine'),
|
||||
paletteHint: t('colorpickerPaletteHint'),
|
||||
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
|
||||
embedderCallback: state => {
|
||||
['hexUppercase', 'color']
|
||||
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
|
||||
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
|
||||
},
|
||||
get maxHeight() {
|
||||
return prefs.get('editor.colorpicker.maxHeight');
|
||||
},
|
||||
set maxHeight(h) {
|
||||
prefs.set('editor.colorpicker.maxHeight', h);
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (defaults.extraKeys) {
|
||||
delete defaults.extraKeys[keyName];
|
||||
options.classList.remove('ignore-pref');
|
||||
lint.classList.remove('ignore-pref');
|
||||
if (prefs.get('editor.options.expanded')) {
|
||||
options.setAttribute('open', '');
|
||||
}
|
||||
if (prefs.get('editor.lint.expanded')) {
|
||||
lint.setAttribute('open', '');
|
||||
}
|
||||
}
|
||||
cmFactory.globalSetOption('colorpicker', defaults.colorpicker);
|
||||
}, {runNow: true});
|
||||
|
||||
$('#colorpicker-settings').onclick = function (event) {
|
||||
event.preventDefault();
|
||||
const input = createHotkeyInput('editor.colorpicker.hotkey', {onDone: () => helpPopup.close()});
|
||||
const popup = helpPopup.show(t('helpKeyMapHotkey'), input);
|
||||
const bounds = this.getBoundingClientRect();
|
||||
popup.style.left = bounds.right + 10 + 'px';
|
||||
popup.style.top = bounds.top - popup.clientHeight / 2 + 'px';
|
||||
popup.style.right = 'auto';
|
||||
$('input', popup).focus();
|
||||
};
|
||||
|
||||
function invokeColorpicker(cm) {
|
||||
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
function isWindowMaximized() {
|
||||
return (
|
||||
window.screenX <= 0 &&
|
||||
window.screenY <= 0 &&
|
||||
window.outerWidth >= screen.availWidth &&
|
||||
window.outerHeight >= screen.availHeight &&
|
||||
|
||||
window.screenX > -10 &&
|
||||
window.screenY > -10 &&
|
||||
window.outerWidth < screen.availWidth + 10 &&
|
||||
window.outerHeight < screen.availHeight + 10
|
||||
);
|
||||
}
|
||||
|
||||
function toggleContextMenuDelete(event) {
|
||||
if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
|
||||
chrome.contextMenus.update('editor.contextDelete', {
|
||||
enabled: Boolean(
|
||||
this.selectionStart !== this.selectionEnd ||
|
||||
this.somethingSelected && this.somethingSelected()
|
||||
),
|
||||
}, ignoreChromeError);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,176 +1,89 @@
|
|||
/* global createWorkerApi */// worker-util.js
|
||||
/* global importScripts workerUtil CSSLint require metaParser */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
let sugarss = false;
|
||||
importScripts('/js/worker-util.js');
|
||||
const {createAPI, loadScript} = workerUtil;
|
||||
|
||||
/** @namespace EditorWorker */
|
||||
createWorkerApi({
|
||||
|
||||
async csslint(code, config) {
|
||||
require(['/js/csslint/parserlib', '/js/csslint/csslint']); /* global CSSLint */
|
||||
return CSSLint
|
||||
.verify(code, config).messages
|
||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||
},
|
||||
|
||||
getCssPropsValues() {
|
||||
require(['/js/csslint/parserlib']); /* global parserlib */
|
||||
const {
|
||||
css: {Colors, GlobalKeywords, Properties},
|
||||
util: {describeProp},
|
||||
} = parserlib;
|
||||
const namedColors = Object.keys(Colors);
|
||||
const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g;
|
||||
const res = {};
|
||||
// moving vendor-prefixed props to the end
|
||||
const cmp = (a, b) => a[0] === '-' && b[0] !== '-' ? 1 : a < b ? -1 : a > b;
|
||||
for (const [k, v] of Object.entries(Properties)) {
|
||||
res[k] = false;
|
||||
if (typeof v === 'string') {
|
||||
let last = '';
|
||||
const uniq = [];
|
||||
// strip definitions of function arguments
|
||||
const desc = describeProp(v).replace(/([-\w]+)\(.*?\)/g, 'z-$1');
|
||||
const descNoColors = desc.replace(/<named-color>/g, '');
|
||||
// add a prefix to functions to group them at the end
|
||||
const words = descNoColors.split(rxNonWord).sort(cmp);
|
||||
for (let w of words) {
|
||||
if (w.startsWith('z-')) w = w.slice(2) + '(';
|
||||
if (w !== last) uniq.push(last = w);
|
||||
}
|
||||
if (desc !== descNoColors) uniq.push(...namedColors);
|
||||
if (uniq.length) res[k] = uniq;
|
||||
}
|
||||
}
|
||||
return {all: res, global: GlobalKeywords};
|
||||
},
|
||||
|
||||
getRules(linter) {
|
||||
return ruleRetriever[linter](); // eslint-disable-line no-use-before-define
|
||||
},
|
||||
|
||||
metalint(code) {
|
||||
require(['/js/meta-parser']); /* global metaParser */
|
||||
const result = metaParser.lint(code);
|
||||
// extract needed info
|
||||
result.errors = result.errors.map(err => ({
|
||||
createAPI({
|
||||
csslint: (code, config) => {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
|
||||
return CSSLint.verify(code, config).messages
|
||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||
},
|
||||
stylelint: (code, config) => {
|
||||
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||
return require('stylelint').lint({code, config});
|
||||
},
|
||||
metalint: code => {
|
||||
loadScript(
|
||||
'/js/polyfill.js',
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
);
|
||||
const result = metaParser.lint(code);
|
||||
// extract needed info
|
||||
result.errors = result.errors.map(err =>
|
||||
({
|
||||
code: err.code,
|
||||
args: err.args,
|
||||
message: err.message,
|
||||
index: err.index,
|
||||
}));
|
||||
return result;
|
||||
},
|
||||
index: err.index
|
||||
})
|
||||
);
|
||||
return result;
|
||||
},
|
||||
getStylelintRules,
|
||||
getCsslintRules
|
||||
});
|
||||
|
||||
async stylelint(opts) {
|
||||
require(['/vendor/stylelint-bundle/stylelint-bundle.min']); /* global stylelint */
|
||||
// Stylus-lang allows a trailing ";" but sugarss doesn't, so we monkeypatch it
|
||||
stylelint.SugarSSParser.prototype.checkSemicolon = tt => {
|
||||
while (tt.length && tt[tt.length - 1][0] === ';') tt.pop();
|
||||
};
|
||||
for (const pass of opts.mode === 'stylus' ? [sugarss, !sugarss] : [-1]) {
|
||||
/* We try sugarss (for indented stylus-lang), then css mode, switching them on failure,
|
||||
* so that the succeeding syntax will be used next time first. */
|
||||
opts.config.customSyntax = !pass ? 'sugarss' : '';
|
||||
try {
|
||||
const res = await stylelint.createLinter(opts)._lintSource(opts);
|
||||
if (pass !== -1) sugarss = pass;
|
||||
return collectStylelintResults(res, opts);
|
||||
} catch (e) {
|
||||
const fatal = pass === -1 ||
|
||||
!pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e) ||
|
||||
pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(`${e}${e.stack}`);
|
||||
if (fatal) {
|
||||
return [{
|
||||
from: {line: e.line - 1, ch: e.column - 1},
|
||||
to: {line: e.line - 1, ch: e.column - 1},
|
||||
message: e.reason,
|
||||
severity: 'error',
|
||||
rule: e.name,
|
||||
}];
|
||||
}
|
||||
}
|
||||
function getCsslintRules() {
|
||||
loadScript('/vendor-overwrites/csslint/csslint.js');
|
||||
return CSSLint.getRules().map(rule => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (typeof value !== 'function') {
|
||||
output[key] = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const ruleRetriever = {
|
||||
|
||||
csslint() {
|
||||
require(['/js/csslint/csslint']);
|
||||
return CSSLint.getRuleList().map(rule => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (typeof value !== 'function') {
|
||||
output[key] = value;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
});
|
||||
},
|
||||
|
||||
stylelint() {
|
||||
require(['/vendor/stylelint-bundle/stylelint-bundle.min']);
|
||||
const options = {};
|
||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||
for (const [id, rule] of Object.entries(stylelint.rules)) {
|
||||
const ruleCode = `${rule()}`;
|
||||
const sets = [];
|
||||
let m, mStr;
|
||||
while ((m = rxPossible.exec(ruleCode))) {
|
||||
const possible = m[1];
|
||||
const set = [];
|
||||
while ((mStr = rxString.exec(possible))) {
|
||||
const s = mStr[1];
|
||||
if (s.includes(' ')) {
|
||||
set.push(...s.split(/\s+/));
|
||||
} else {
|
||||
set.push(s);
|
||||
}
|
||||
}
|
||||
if (possible.includes('ignoreAtRules')) {
|
||||
set.push('ignoreAtRules');
|
||||
}
|
||||
if (possible.includes('ignoreShorthands')) {
|
||||
set.push('ignoreShorthands');
|
||||
}
|
||||
if (set.length) {
|
||||
sets.push(set);
|
||||
}
|
||||
}
|
||||
options[id] = sets;
|
||||
}
|
||||
return options;
|
||||
},
|
||||
};
|
||||
|
||||
function collectStylelintResults({messages}, {mode}) {
|
||||
/* We hide nonfatal "//" warnings since we lint with sugarss without applying @preprocessor.
|
||||
* We can't easily pre-remove "//" comments which may be inside strings, comments, url(), etc.
|
||||
* And even if we did, it'd be wrong to hide potential bugs in stylus-lang like #1460 */
|
||||
const isLess = mode === 'text/x-less';
|
||||
const slashCommentAllowed = isLess || mode === 'stylus';
|
||||
const res = [];
|
||||
for (const m of messages) {
|
||||
if (/deprecation|invalidOption/.test(m.stylelintType)) {
|
||||
continue;
|
||||
}
|
||||
const {rule} = m;
|
||||
const msg = m.text.replace(/^Unexpected\s+/, '').replace(` (${rule})`, '');
|
||||
if (slashCommentAllowed && msg.includes('"//"') ||
|
||||
isLess && /^unknown at-rule "@[-\w]+:"/.test(msg) /* LESS variables */) {
|
||||
continue;
|
||||
}
|
||||
res.push({
|
||||
from: {line: m.line - 1, ch: m.column - 1},
|
||||
to: {line: m.endLine - 1, ch: m.endColumn - 1},
|
||||
message: msg[0].toUpperCase() + msg.slice(1),
|
||||
severity: m.severity,
|
||||
rule,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
function getStylelintRules() {
|
||||
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||
const stylelint = require('stylelint');
|
||||
const options = {};
|
||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||
for (const id of Object.keys(stylelint.rules)) {
|
||||
const ruleCode = String(stylelint.rules[id]);
|
||||
const sets = [];
|
||||
let m, mStr;
|
||||
while ((m = rxPossible.exec(ruleCode))) {
|
||||
const possible = m[1];
|
||||
const set = [];
|
||||
while ((mStr = rxString.exec(possible))) {
|
||||
const s = mStr[1];
|
||||
if (s.includes(' ')) {
|
||||
set.push(...s.split(/\s+/));
|
||||
} else {
|
||||
set.push(s);
|
||||
}
|
||||
}
|
||||
if (possible.includes('ignoreAtRules')) {
|
||||
set.push('ignoreAtRules');
|
||||
}
|
||||
if (possible.includes('ignoreShorthands')) {
|
||||
set.push('ignoreShorthands');
|
||||
}
|
||||
if (set.length) {
|
||||
sets.push(set);
|
||||
}
|
||||
}
|
||||
if (sets.length) {
|
||||
options[id] = sets;
|
||||
}
|
||||
}
|
||||
})();
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
/* global $ $create $remove getEventKeyName */// dom.js
|
||||
/* global CodeMirror */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
const ID = 'popup-iframe';
|
||||
const SEL = '#' + ID;
|
||||
const URL = chrome.runtime.getManifest().browser_action.default_popup;
|
||||
const POPUP_HOTKEY = 'Shift-Ctrl-Alt-S';
|
||||
/** @type {HTMLIFrameElement} */
|
||||
let frame;
|
||||
let isLoaded;
|
||||
let scrollbarWidth;
|
||||
|
||||
const btn = $create('img', {
|
||||
id: 'popup-button',
|
||||
title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY,
|
||||
onclick: embedPopup,
|
||||
});
|
||||
$.root.appendChild(btn);
|
||||
$.rootCL.add('popup-window');
|
||||
window.on('domReady', () => {
|
||||
document.body.appendChild(btn);
|
||||
// Adding a dummy command to show in keymap help popup
|
||||
CodeMirror.defaults.extraKeys[POPUP_HOTKEY] = 'openStylusPopup';
|
||||
}, {once: true});
|
||||
|
||||
prefs.subscribe('iconset', (_, val) => {
|
||||
const prefix = `images/icon/${val ? 'light/' : ''}`;
|
||||
btn.srcset = `${prefix}16.png 1x,${prefix}32.png 2x`;
|
||||
}, {runNow: true});
|
||||
|
||||
window.on('keydown', e => {
|
||||
if (getEventKeyName(e) === POPUP_HOTKEY) {
|
||||
embedPopup();
|
||||
}
|
||||
});
|
||||
|
||||
function embedPopup() {
|
||||
if ($(SEL)) return;
|
||||
isLoaded = false;
|
||||
scrollbarWidth = 0;
|
||||
frame = $create('iframe', {
|
||||
id: ID,
|
||||
src: URL,
|
||||
height: 600,
|
||||
width: prefs.get('popupWidth'),
|
||||
onload: initFrame,
|
||||
});
|
||||
window.on('mousedown', removePopup);
|
||||
document.body.appendChild(frame);
|
||||
}
|
||||
|
||||
function initFrame() {
|
||||
frame = this;
|
||||
frame.focus();
|
||||
const pw = frame.contentWindow;
|
||||
const body = pw.document.body;
|
||||
pw.on('keydown', removePopupOnEsc);
|
||||
pw.close = removePopup;
|
||||
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
|
||||
$create('div', {style: {height: '1px', marginTop: '-1px'}})
|
||||
));
|
||||
new pw.MutationObserver(onMutation).observe(body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
});
|
||||
}
|
||||
|
||||
function onMutation() {
|
||||
const body = frame.contentDocument.body;
|
||||
const bs = body.style;
|
||||
const w = parseFloat(bs.minWidth || bs.width) + (scrollbarWidth || 0);
|
||||
const h = parseFloat(bs.minHeight || body.offsetHeight);
|
||||
if (frame.width - w) frame.width = w;
|
||||
if (frame.height - h) frame.height = h;
|
||||
}
|
||||
|
||||
function onIntersect([e]) {
|
||||
const pw = frame.contentWindow;
|
||||
const el = pw.document.scrollingElement;
|
||||
const h = e.isIntersecting && !pw.scrollY ? el.offsetHeight : el.scrollHeight;
|
||||
const hasSB = h > el.offsetHeight;
|
||||
const {width} = e.boundingClientRect;
|
||||
frame.height = h;
|
||||
if (!hasSB !== !scrollbarWidth || frame.width - width) {
|
||||
scrollbarWidth = hasSB ? width - el.offsetWidth : 0;
|
||||
frame.width = width + scrollbarWidth;
|
||||
}
|
||||
if (!isLoaded) {
|
||||
isLoaded = true;
|
||||
frame.dataset.loaded = '';
|
||||
}
|
||||
}
|
||||
|
||||
function removePopup() {
|
||||
frame = null;
|
||||
$remove(SEL);
|
||||
window.off('mousedown', removePopup);
|
||||
}
|
||||
|
||||
function removePopupOnEsc(e) {
|
||||
if (getEventKeyName(e) === 'Escape') {
|
||||
removePopup();
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -61,7 +61,7 @@
|
|||
border: none;
|
||||
background-color: white;
|
||||
font-weight: bold;
|
||||
white-space: pre; /* issue #1000 */
|
||||
white-space: nowrap;
|
||||
color: currentColor; /* use the current theme's color instead of UserAgent's CSS */
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -183,8 +183,7 @@
|
|||
|
||||
/*********** CM search highlight restyling, which shouldn't need color variables ****************/
|
||||
body.find-open .search-target-editor {
|
||||
box-shadow: 0 0 0 1px hsl(33, 100%, 50%), 0 0 3px hsla(33, 100%, 50%, .4);
|
||||
/* same as our global.css focus rule */
|
||||
outline-color: darkorange !important;
|
||||
}
|
||||
|
||||
body.find-open .cm-searching {
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
/* global $ $$ $create $remove focusAccessibility setInputValue toggleDataset */// dom.js
|
||||
/* global CodeMirror */
|
||||
/* global chromeLocal */// storage-util.js
|
||||
/* global colorMimicry */
|
||||
/* global debounce stringAsRegExp tryRegExp */// toolbox.js
|
||||
/* global editor */
|
||||
/* global t */// localization.js
|
||||
/* global CodeMirror focusAccessibility colorMimicry editor
|
||||
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
require(['/edit/global-search.css']);
|
||||
onDOMready().then(() => {
|
||||
|
||||
//region Constants and state
|
||||
|
||||
|
@ -16,6 +10,7 @@
|
|||
const ANNOTATE_SCROLLBAR_DELAY = 350;
|
||||
const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3};
|
||||
const STORAGE_UPDATE_DELAY = 500;
|
||||
const SCROLL_REVEAL_MIN_PX = 50;
|
||||
|
||||
const DIALOG_SELECTOR = '#search-replace-dialog';
|
||||
const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style';
|
||||
|
@ -27,7 +22,6 @@
|
|||
const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
|
||||
|
||||
const state = {
|
||||
firstRun: true,
|
||||
// used for case-sensitive matching directly
|
||||
find: '',
|
||||
// used when /re/ is detected or for case-insensitive matching
|
||||
|
@ -54,7 +48,7 @@
|
|||
|
||||
undoHistory: [],
|
||||
|
||||
searchInApplies: !editor.isUsercss,
|
||||
searchInApplies: !document.documentElement.classList.contains('usercss'),
|
||||
};
|
||||
|
||||
//endregion
|
||||
|
@ -70,12 +64,10 @@
|
|||
if (found) {
|
||||
const target = $('.' + TARGET_CLASS);
|
||||
const cm = target.CodeMirror;
|
||||
/* Since this runs in `keydown` event we have to delay focusing
|
||||
* to prevent CodeMirror from seeing and handling the key */
|
||||
setTimeout(() => (cm || target).focus());
|
||||
(cm || target).focus();
|
||||
if (cm) {
|
||||
const {from, to} = cm.state.search.searchPos;
|
||||
cm.jumpToPos(from, to);
|
||||
const pos = cm.state.search.searchPos;
|
||||
cm.setSelection(pos.from, pos.to);
|
||||
}
|
||||
}
|
||||
destroyDialog({restoreFocus: !found});
|
||||
|
@ -86,7 +78,7 @@
|
|||
doReplace();
|
||||
return;
|
||||
}
|
||||
return !focusAccessibility.closest(event.target);
|
||||
return !event.target.closest(focusAccessibility.ELEMENTS.join(','));
|
||||
},
|
||||
'Esc': () => {
|
||||
destroyDialog({restoreFocus: true});
|
||||
|
@ -107,7 +99,7 @@
|
|||
state.lastFind = '';
|
||||
toggleDataset(this, 'enabled', !state.icase);
|
||||
doSearch({canAdvance: false});
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -133,17 +125,17 @@
|
|||
},
|
||||
onfocusout() {
|
||||
if (!state.dialog.contains(document.activeElement)) {
|
||||
state.dialog.on('focusin', EVENTS.onfocusin);
|
||||
state.dialog.off('focusout', EVENTS.onfocusout);
|
||||
state.dialog.addEventListener('focusin', EVENTS.onfocusin);
|
||||
state.dialog.removeEventListener('focusout', EVENTS.onfocusout);
|
||||
}
|
||||
},
|
||||
onfocusin() {
|
||||
state.dialog.on('focusout', EVENTS.onfocusout);
|
||||
state.dialog.off('focusin', EVENTS.onfocusin);
|
||||
state.dialog.addEventListener('focusout', EVENTS.onfocusout);
|
||||
state.dialog.removeEventListener('focusin', EVENTS.onfocusin);
|
||||
trimUndoHistory();
|
||||
enableUndoButton(state.undoHistory.length);
|
||||
if (state.find) doSearch({canAdvance: false});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const DIALOG_PROPS = {
|
||||
|
@ -159,7 +151,7 @@
|
|||
state.replace = this.value;
|
||||
adjustTextareaSize(this);
|
||||
debounce(writeStorage, STORAGE_UPDATE_DELAY);
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -176,7 +168,7 @@
|
|||
replace(cm) {
|
||||
state.reverse = false;
|
||||
focusDialog('replace', cm);
|
||||
},
|
||||
}
|
||||
};
|
||||
COMMANDS.replaceAll = COMMANDS.replace;
|
||||
|
||||
|
@ -184,6 +176,7 @@
|
|||
|
||||
Object.assign(CodeMirror.commands, COMMANDS);
|
||||
readStorage();
|
||||
return;
|
||||
|
||||
//region Find
|
||||
|
||||
|
@ -248,7 +241,6 @@
|
|||
} else {
|
||||
showTally(0, 0);
|
||||
}
|
||||
state.firstRun = false;
|
||||
return found;
|
||||
}
|
||||
|
||||
|
@ -567,16 +559,15 @@
|
|||
|
||||
function createDialog(type) {
|
||||
state.originalFocus = document.activeElement;
|
||||
state.firstRun = true;
|
||||
|
||||
const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
|
||||
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true);
|
||||
Object.assign(dialog, DIALOG_PROPS.dialog);
|
||||
dialog.on('focusout', EVENTS.onfocusout);
|
||||
dialog.addEventListener('focusout', EVENTS.onfocusout);
|
||||
dialog.dataset.type = type;
|
||||
dialog.style.pointerEvents = 'auto';
|
||||
|
||||
const content = $('[data-type="content"]', dialog);
|
||||
content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
|
||||
content.parentNode.replaceChild(template[type].cloneNode(true), content);
|
||||
|
||||
createInput(0, 'input', state.find);
|
||||
createInput(1, 'input2', state.replace);
|
||||
|
@ -584,11 +575,11 @@
|
|||
state.tally = $('[data-type="tally"]', dialog);
|
||||
|
||||
const colors = {
|
||||
body: colorMimicry(document.body, {bg: 'backgroundColor'}),
|
||||
input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
|
||||
icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
|
||||
body: colorMimicry.get(document.body, {bg: 'backgroundColor'}),
|
||||
input: colorMimicry.get($('input:not(:disabled)'), {bg: 'backgroundColor'}),
|
||||
icon: colorMimicry.get($$('svg.info')[1], {fill: 'fill'}),
|
||||
};
|
||||
$.root.appendChild(
|
||||
document.documentElement.appendChild(
|
||||
$(DIALOG_STYLE_SELECTOR) ||
|
||||
$create('style' + DIALOG_STYLE_SELECTOR)
|
||||
).textContent = `
|
||||
|
@ -607,10 +598,10 @@
|
|||
}
|
||||
#search-replace-dialog[data-type="replace"] button:hover svg,
|
||||
#search-replace-dialog svg:hover {
|
||||
fill: var(--cmin);
|
||||
fill: inherit;
|
||||
}
|
||||
#search-replace-dialog [data-action="case"]:hover {
|
||||
color: var(--cmin);
|
||||
color: inherit;
|
||||
}
|
||||
#search-replace-dialog [data-action="clear"] {
|
||||
background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'};
|
||||
|
@ -639,14 +630,14 @@
|
|||
input.value = value;
|
||||
Object.assign(input, DIALOG_PROPS[name]);
|
||||
|
||||
input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
|
||||
input.parentElement.appendChild(template.clearSearch.cloneNode(true));
|
||||
$('[data-action]', input.parentElement)._input = input;
|
||||
}
|
||||
|
||||
|
||||
function destroyDialog({restoreFocus = false} = {}) {
|
||||
state.input = null;
|
||||
$remove(DIALOG_SELECTOR);
|
||||
$.remove(DIALOG_SELECTOR);
|
||||
debounce.unregister(doSearch);
|
||||
makeTargetVisible(null);
|
||||
if (restoreFocus) {
|
||||
|
@ -680,7 +671,7 @@
|
|||
el.style.width = newWidth + 'px';
|
||||
}
|
||||
const numLines = el.value.split('\n').length;
|
||||
if (numLines !== Number(el.rows)) {
|
||||
if (numLines !== parseInt(el.rows)) {
|
||||
el.rows = numLines;
|
||||
}
|
||||
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
|
||||
|
@ -775,22 +766,25 @@
|
|||
|
||||
// scrolls the editor to reveal the match
|
||||
function makeMatchVisible(cm, searchCursor) {
|
||||
const canFocus = !state.firstRun && (!state.dialog || !state.dialog.contains(document.activeElement));
|
||||
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
||||
state.cm = cm;
|
||||
|
||||
// scroll within the editor
|
||||
const pos = searchCursor.pos;
|
||||
Object.assign(getStateSafe(cm), {
|
||||
cursorPos: {
|
||||
from: cm.getCursor('from'),
|
||||
to: cm.getCursor('to'),
|
||||
},
|
||||
searchPos: pos,
|
||||
searchPos: searchCursor.pos,
|
||||
unclosedOp: !cm.curOp,
|
||||
});
|
||||
if (!cm.curOp) cm.startOperation();
|
||||
if (!state.firstRun) {
|
||||
cm.jumpToPos(pos.from, pos.to);
|
||||
}
|
||||
if (canFocus) cm.setSelection(searchCursor.pos.from, searchCursor.pos.to);
|
||||
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX);
|
||||
|
||||
// scroll to the editor itself
|
||||
editor.scrollToEditor(cm);
|
||||
|
||||
// focus or expose as the current search target
|
||||
clearMarker();
|
||||
if (canFocus) {
|
||||
|
@ -799,6 +793,7 @@
|
|||
} else {
|
||||
makeTargetVisible(cm.display.wrapper);
|
||||
// mark the match
|
||||
const pos = searchCursor.pos;
|
||||
state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, {
|
||||
className: MATCH_CLASS,
|
||||
clearOnEnter: true,
|
||||
|
@ -828,13 +823,11 @@
|
|||
if (numApplies === undefined && state.searchInApplies && state.numApplies < 0) {
|
||||
numApplies = 0;
|
||||
const elements = state.find ? document.getElementsByClassName(APPLIES_VALUE_CLASS) : [];
|
||||
const {rx} = state;
|
||||
for (const el of elements) {
|
||||
const value = el.value;
|
||||
if (rx) {
|
||||
rx.lastIndex = 0;
|
||||
// preventing an infinite loop if matched an empty string and didn't advance
|
||||
for (let m; (m = rx.exec(value)) && ++numApplies && rx.lastIndex > m.index;) { /* NOP */ }
|
||||
if (state.rx) {
|
||||
state.rx.lastIndex = 0;
|
||||
while (state.rx.exec(value)) numApplies++;
|
||||
} else {
|
||||
let i = -1;
|
||||
while ((i = value.indexOf(state.find, i + 1)) >= 0) numApplies++;
|
||||
|
@ -876,6 +869,15 @@
|
|||
}
|
||||
|
||||
|
||||
function toggleDataset(el, prop, state) {
|
||||
if (state) {
|
||||
el.dataset[prop] = '';
|
||||
} else {
|
||||
delete el.dataset[prop];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function saveWindowScrollPos() {
|
||||
state.scrollX = window.scrollX;
|
||||
state.scrollY = window.scrollY;
|
||||
|
@ -896,9 +898,7 @@
|
|||
|
||||
// produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
|
||||
function radiateArray(arr, focalIndex) {
|
||||
const focus = arr[focalIndex];
|
||||
if (!focus) return arr;
|
||||
const result = [focus];
|
||||
const result = [arr[focalIndex]];
|
||||
const len = arr.length;
|
||||
for (let i = 1; i < len; i++) {
|
||||
if (focalIndex + i < len) {
|
||||
|
@ -913,7 +913,7 @@
|
|||
|
||||
|
||||
function readStorage() {
|
||||
chromeLocal.getValue('editor').then((editor = {}) => {
|
||||
chrome.storage.local.get('editor', ({editor = {}}) => {
|
||||
state.find = editor.find || '';
|
||||
state.replace = editor.replace || '';
|
||||
state.icase = editor.icase || state.icase;
|
||||
|
@ -922,13 +922,28 @@
|
|||
|
||||
|
||||
function writeStorage() {
|
||||
chromeLocal.getValue('editor').then((editor = {}) =>
|
||||
chromeLocal.setValue('editor', Object.assign(editor, {
|
||||
find: state.find,
|
||||
replace: state.replace,
|
||||
icase: state.icase,
|
||||
})));
|
||||
chrome.storage.local.get('editor', ({editor}) =>
|
||||
chrome.storage.local.set({
|
||||
editor: Object.assign(editor || {}, {
|
||||
find: state.find,
|
||||
replace: state.replace,
|
||||
icase: state.icase,
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function setInputValue(input, value) {
|
||||
input.focus();
|
||||
input.select();
|
||||
// using execCommand to add to the input's undo history
|
||||
document.execCommand(value ? 'insertText' : 'delete', false, value);
|
||||
// some versions of Firefox ignore execCommand
|
||||
if (input.value !== value) {
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
})();
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user