Merge branch 'master' into dev-color-scheme
This commit is contained in:
commit
241f5b9f41
|
|
@ -1,4 +1,2 @@
|
||||||
vendor/
|
vendor/
|
||||||
vendor-overwrites/*
|
vendor-overwrites/
|
||||||
!vendor-overwrites/colorpicker
|
|
||||||
!vendor-overwrites/csslint
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ env:
|
||||||
es6: true
|
es6: true
|
||||||
webextensions: true
|
webextensions: true
|
||||||
|
|
||||||
|
globals:
|
||||||
|
require: readonly # in polyfill.js
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
accessor-pairs: [2]
|
accessor-pairs: [2]
|
||||||
array-bracket-spacing: [2, never]
|
array-bracket-spacing: [2, never]
|
||||||
|
|
@ -19,7 +22,7 @@ rules:
|
||||||
brace-style: [2, 1tbs, {allowSingleLine: false}]
|
brace-style: [2, 1tbs, {allowSingleLine: false}]
|
||||||
camelcase: [2, {properties: never}]
|
camelcase: [2, {properties: never}]
|
||||||
class-methods-use-this: [2]
|
class-methods-use-this: [2]
|
||||||
comma-dangle: [0]
|
comma-dangle: [2, {arrays: always-multiline, objects: always-multiline}]
|
||||||
comma-spacing: [2, {before: false, after: true}]
|
comma-spacing: [2, {before: false, after: true}]
|
||||||
comma-style: [2, last]
|
comma-style: [2, last]
|
||||||
complexity: [0]
|
complexity: [0]
|
||||||
|
|
@ -42,7 +45,15 @@ rules:
|
||||||
id-blacklist: [0]
|
id-blacklist: [0]
|
||||||
id-length: [0]
|
id-length: [0]
|
||||||
id-match: [0]
|
id-match: [0]
|
||||||
indent-legacy: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
|
indent: [2, 2, {
|
||||||
|
SwitchCase: 1,
|
||||||
|
ignoreComments: true,
|
||||||
|
ignoredNodes: [
|
||||||
|
"TemplateLiteral > *",
|
||||||
|
"ConditionalExpression",
|
||||||
|
"ForStatement"
|
||||||
|
]
|
||||||
|
}]
|
||||||
jsx-quotes: [0]
|
jsx-quotes: [0]
|
||||||
key-spacing: [0]
|
key-spacing: [0]
|
||||||
keyword-spacing: [2]
|
keyword-spacing: [2]
|
||||||
|
|
@ -86,7 +97,7 @@ rules:
|
||||||
no-empty: [2, {allowEmptyCatch: true}]
|
no-empty: [2, {allowEmptyCatch: true}]
|
||||||
no-eq-null: [0]
|
no-eq-null: [0]
|
||||||
no-eval: [2]
|
no-eval: [2]
|
||||||
no-ex-assign: [2]
|
no-ex-assign: [0]
|
||||||
no-extend-native: [2]
|
no-extend-native: [2]
|
||||||
no-extra-bind: [2]
|
no-extra-bind: [2]
|
||||||
no-extra-boolean-cast: [2]
|
no-extra-boolean-cast: [2]
|
||||||
|
|
@ -136,6 +147,9 @@ rules:
|
||||||
no-proto: [2]
|
no-proto: [2]
|
||||||
no-redeclare: [2]
|
no-redeclare: [2]
|
||||||
no-regex-spaces: [2]
|
no-regex-spaces: [2]
|
||||||
|
no-restricted-globals: [2, name, event]
|
||||||
|
# `name` and `event` (in Chrome) are built-in globals
|
||||||
|
# but we don't use these globals so it's most likely a mistake/typo
|
||||||
no-restricted-imports: [0]
|
no-restricted-imports: [0]
|
||||||
no-restricted-modules: [2, domain, freelist, smalloc, sys]
|
no-restricted-modules: [2, domain, freelist, smalloc, sys]
|
||||||
no-restricted-syntax: [2, WithStatement]
|
no-restricted-syntax: [2, WithStatement]
|
||||||
|
|
@ -163,7 +177,7 @@ rules:
|
||||||
no-unreachable: [2]
|
no-unreachable: [2]
|
||||||
no-unsafe-finally: [2]
|
no-unsafe-finally: [2]
|
||||||
no-unsafe-negation: [2]
|
no-unsafe-negation: [2]
|
||||||
no-unused-expressions: [1]
|
no-unused-expressions: [2]
|
||||||
no-unused-labels: [0]
|
no-unused-labels: [0]
|
||||||
no-unused-vars: [2, {args: after-used}]
|
no-unused-vars: [2, {args: after-used}]
|
||||||
no-use-before-define: [2, nofunc]
|
no-use-before-define: [2, nofunc]
|
||||||
|
|
@ -189,7 +203,7 @@ rules:
|
||||||
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
|
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
|
||||||
quote-props: [0]
|
quote-props: [0]
|
||||||
quotes: [1, single, avoid-escape]
|
quotes: [1, single, avoid-escape]
|
||||||
radix: [2, as-needed]
|
radix: [2, always]
|
||||||
require-jsdoc: [0]
|
require-jsdoc: [0]
|
||||||
require-yield: [2]
|
require-yield: [2]
|
||||||
semi-spacing: [2, {before: false, after: true}]
|
semi-spacing: [2, {before: false, after: true}]
|
||||||
|
|
@ -220,3 +234,7 @@ overrides:
|
||||||
webextensions: false
|
webextensions: false
|
||||||
parserOptions:
|
parserOptions:
|
||||||
ecmaVersion: 2017
|
ecmaVersion: 2017
|
||||||
|
|
||||||
|
- files: ["**/*worker*.js"]
|
||||||
|
env:
|
||||||
|
worker: true
|
||||||
|
|
|
||||||
9
.github/ISSUE_TEMPLATE.md
vendored
9
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -1,9 +0,0 @@
|
||||||
* **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
Normal file
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
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. -->
|
||||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
|
@ -3,18 +3,12 @@ on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
node: ['10', '12', '14']
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: '14'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -21,12 +21,9 @@ Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExt
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|
Manager | Editor | Popup search | Popup config | Manager config | Options
|
||||||

|
-|-|-|-|-|-
|
||||||

|
 |  |  |  |  | 
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
|
|
||||||
|
|
@ -47,15 +44,15 @@ See our [contributing](./.github/CONTRIBUTING.md) page for more details.
|
||||||
|
|
||||||
## License
|
## 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)
|
Copyright © 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
|
||||||
|
|
||||||
Current Stylus:
|
Current Stylus:
|
||||||
|
|
||||||
Copyright © 2017-2019 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
|
Copyright © 2017-2019 [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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
{
|
{
|
||||||
|
"InaccessibleFileHint": {
|
||||||
|
"message": "Stylus لا يستطيع الوصول الى بعض انواع الملفات ( ملفات pdf و json )"
|
||||||
|
},
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "كتابة نمط جديد",
|
"message": "كتابة نمط جديد"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "إضافة نمط",
|
"message": "إضافة نمط"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "إضافة",
|
"message": "إضافة"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "ينطبق على: $applies$",
|
"message": "ينطبق على: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,84 +20,64 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "والمزيد",
|
"message": "و المزيد"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "عناوين URL في النطاق",
|
"message": "عناوين URL في النطاق"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesHelp": {
|
"appliesHelp": {
|
||||||
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم.",
|
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم."
|
||||||
"description": "Help text for 'applies to' section"
|
|
||||||
},
|
},
|
||||||
"appliesLabel": {
|
"appliesLabel": {
|
||||||
"message": "ينطبق على",
|
"message": "ينطبق على"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"appliesRegexpOption": {
|
||||||
"message": "عناوين URL التي تطابق regexp",
|
"message": "عناوين URL التي تطابق regexp"
|
||||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"appliesRemove": {
|
||||||
"message": "إزالة",
|
"message": "إزالة"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "تحديد",
|
"message": "تحديد"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "كل شيء",
|
"message": "كل شيء"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlOption": {
|
"appliesUrlOption": {
|
||||||
"message": "عنوان URL",
|
"message": "عنوان URL"
|
||||||
"description": "Option to make the style apply to the entered string as a URL"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"appliesUrlPrefixOption": {
|
||||||
"message": "عناوين URL البادئة بـ",
|
"message": "عناوين URL البادئة بـ"
|
||||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"checkAllUpdates": {
|
||||||
"message": "البحث عن تحديثات لكل الأنماط",
|
"message": "البحث عن تحديثات لكل الأنماط"
|
||||||
"description": "Label for the button to check all styles for updates"
|
|
||||||
},
|
},
|
||||||
"checkForUpdate": {
|
"checkForUpdate": {
|
||||||
"message": "البحث عن تحديث",
|
"message": "البحث عن تحديث"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "جارٍ البحث...",
|
"message": "جارٍ البحث..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"deleteStyleConfirm": {
|
||||||
"message": "هل تريد بالتأكيد حذف هذا النمط؟",
|
"message": "هل تريد بالتأكيد حذف هذا النمط؟"
|
||||||
"description": "Confirmation before deleting a style"
|
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"deleteStyleLabel": {
|
||||||
"message": "حذف",
|
"message": "حذف"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى.",
|
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى."
|
||||||
"description": "Extension description"
|
|
||||||
},
|
},
|
||||||
"disableStyleLabel": {
|
"disableStyleLabel": {
|
||||||
"message": "تعطيل",
|
"message": "تعطيل"
|
||||||
"description": "Label for the button to disable a style"
|
|
||||||
},
|
},
|
||||||
"editStyleHeading": {
|
"editStyleHeading": {
|
||||||
"message": "تعديل النمط",
|
"message": "تعديل النمط"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "تعديل",
|
"message": "تعديل"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"editStyleTitle": {
|
"editStyleTitle": {
|
||||||
"message": "تعديل النمط $stylename$",
|
"message": "تعديل النمط $stylename$",
|
||||||
"description": "Title of the page for editing styles",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -106,60 +85,46 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"enableStyleLabel": {
|
||||||
"message": "تمكين",
|
"message": "تمكين"
|
||||||
"description": "Label for the button to enable a style"
|
|
||||||
},
|
},
|
||||||
"findStylesForSite": {
|
"findStylesForSite": {
|
||||||
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا",
|
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا"
|
||||||
"description": "Text for a link that gets a list of styles for the current site"
|
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "مساعدة",
|
"message": "مساعدة"
|
||||||
"description": "Alternate text for help buttons"
|
|
||||||
},
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "تثبيت التحديث",
|
"message": "تثبيت التحديث"
|
||||||
"description": "Label for the button to install an update for a single style"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "الأنماط المثبتة",
|
"message": "الأنماط المثبتة"
|
||||||
"description": "Heading for the manage page"
|
|
||||||
},
|
},
|
||||||
"noStylesForSite": {
|
"noStylesForSite": {
|
||||||
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا.",
|
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا."
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
|
||||||
},
|
},
|
||||||
"openManage": {
|
"openManage": {
|
||||||
"message": "إدارة الأنماط المثبتة",
|
"message": "إدارة الأنماط المثبتة"
|
||||||
"description": "Link to open the manage page."
|
|
||||||
},
|
},
|
||||||
"sectionAdd": {
|
"sectionAdd": {
|
||||||
"message": "إضافة قسم آخر",
|
"message": "إضافة قسم آخر"
|
||||||
"description": "Label for the button to add a section"
|
|
||||||
},
|
},
|
||||||
"sectionCode": {
|
"sectionCode": {
|
||||||
"message": "الرمز",
|
"message": "الرمز"
|
||||||
"description": "Label for the code for a section"
|
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"sectionRemove": {
|
||||||
"message": "إزالة القسم",
|
"message": "إزالة القسم"
|
||||||
"description": "Label for the button to remove a section"
|
|
||||||
},
|
},
|
||||||
"styleCancelEditLabel": {
|
"styleCancelEditLabel": {
|
||||||
"message": "رجوع للإدارة",
|
"message": "رجوع للإدارة"
|
||||||
"description": "Label for cancel button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleChangesNotSaved": {
|
"styleChangesNotSaved": {
|
||||||
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها.",
|
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها."
|
||||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
|
||||||
},
|
},
|
||||||
"styleEnabledLabel": {
|
"styleEnabledLabel": {
|
||||||
"message": "ممكّن",
|
"message": "ممكّن"
|
||||||
"description": "Label for the enabled state of styles"
|
|
||||||
},
|
},
|
||||||
"styleInstall": {
|
"styleInstall": {
|
||||||
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
|
||||||
"description": "Confirmation when installing a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -167,20 +132,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "أدخل اسمًا",
|
"message": "أدخل اسمًا"
|
||||||
"description": "Error displayed when user saves without providing a name"
|
|
||||||
},
|
},
|
||||||
"styleSaveLabel": {
|
"styleSaveLabel": {
|
||||||
"message": "حفظ",
|
"message": "حفظ"
|
||||||
"description": "Label for save button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"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": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
|
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
|
||||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"code": {
|
"code": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -188,15 +149,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updateCheckFailServerUnreachable": {
|
"updateCheckFailServerUnreachable": {
|
||||||
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه.",
|
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه."
|
||||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
|
||||||
},
|
},
|
||||||
"updateCheckSucceededNoUpdate": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "النمط محدّث.",
|
"message": "النمط محدّث."
|
||||||
"description": "Text that displays when an update check completed and no update is available"
|
|
||||||
},
|
},
|
||||||
"updateCompleted": {
|
"updateCompleted": {
|
||||||
"message": "اكتمل التحديث.",
|
"message": "اكتمل التحديث."
|
||||||
"description": "Text that displays when an update completed"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Писане на нов стил",
|
"message": "Писане на нов стил"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Добавяне на стил",
|
"message": "Добавяне на стил"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Добавяне",
|
"message": "Добавяне"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Приложимо за: $applies$",
|
"message": "Приложимо за: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,196 +17,148 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "и още",
|
"message": "и още"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "Адреси на домейна",
|
"message": "Адреси на домейна"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesHelp": {
|
"appliesHelp": {
|
||||||
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела.",
|
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела."
|
||||||
"description": "Help text for 'applies to' section"
|
|
||||||
},
|
},
|
||||||
"appliesLabel": {
|
"appliesLabel": {
|
||||||
"message": "Приложимо за",
|
"message": "Приложимо за"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"appliesRegexpOption": {
|
||||||
"message": "Адреси, съвпадащи с регулярен израз",
|
"message": "Адреси, съвпадащи с регулярен израз"
|
||||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"appliesRemove": {
|
||||||
"message": "Премахване",
|
"message": "Премахване"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Уточняване",
|
"message": "Уточняване"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Всичко",
|
"message": "Всичко"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlOption": {
|
"appliesUrlOption": {
|
||||||
"message": "Адрес",
|
"message": "Адрес"
|
||||||
"description": "Option to make the style apply to the entered string as a URL"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"appliesUrlPrefixOption": {
|
||||||
"message": "Адреси, започващи с",
|
"message": "Адреси, започващи с"
|
||||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
|
||||||
},
|
},
|
||||||
"applyAllUpdates": {
|
"applyAllUpdates": {
|
||||||
"message": "Прилагане на всички обновления",
|
"message": "Прилагане на всички обновления"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"backupButtons": {
|
"backupButtons": {
|
||||||
"message": "Резервни копия",
|
"message": "Резервни копия"
|
||||||
"description": "Heading for backup"
|
|
||||||
},
|
},
|
||||||
"backupMessage": {
|
"backupMessage": {
|
||||||
"message": "Изберете файл или го влачете до страницата.",
|
"message": "Изберете файл или го влачете до страницата."
|
||||||
"description": "Message for backup"
|
|
||||||
},
|
},
|
||||||
"bckpInstStyles": {
|
"bckpInstStyles": {
|
||||||
"message": "Изнасяне на стилове",
|
"message": "Изнасяне на стилове"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"checkAllUpdates": {
|
||||||
"message": "Проверка на всички стилове за обновления",
|
"message": "Проверка на всички стилове за обновления"
|
||||||
"description": "Label for the button to check all styles for updates"
|
|
||||||
},
|
},
|
||||||
"checkAllUpdatesForce": {
|
"checkAllUpdatesForce": {
|
||||||
"message": "Повторна проверка",
|
"message": "Повторна проверка"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"checkForUpdate": {
|
"checkForUpdate": {
|
||||||
"message": "Проверка за обновления",
|
"message": "Проверка за обновления"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Проверяване...",
|
"message": "Проверяване..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"cm_autocompleteOnTyping": {
|
"cm_autocompleteOnTyping": {
|
||||||
"message": "Автоматично завършване при въвеждане",
|
"message": "Автоматично завършване при въвеждане"
|
||||||
"description": "Label for the checkbox in the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_indentWithTabs": {
|
"cm_indentWithTabs": {
|
||||||
"message": "Подпрозорци с умен отстъп",
|
"message": "Подпрозорци с умен отстъп"
|
||||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_keyMap": {
|
"cm_keyMap": {
|
||||||
"message": "Клавиши",
|
"message": "Клавиши"
|
||||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_lineWrapping": {
|
"cm_lineWrapping": {
|
||||||
"message": "Пренасяне",
|
"message": "Пренасяне"
|
||||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_matchHighlight": {
|
"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": {
|
"cm_matchHighlightSelection": {
|
||||||
"message": "Само избраното",
|
"message": "Само избраното"
|
||||||
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
|
|
||||||
},
|
},
|
||||||
"cm_matchHighlightToken": {
|
"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": {
|
"cm_resizeGripHint": {
|
||||||
"message": "Щракнете два пъти за възстановяване/увеличаване на височината",
|
"message": "Щракнете два пъти за възстановяване/увеличаване на височината"
|
||||||
"description": "Tooltip for the resize grip in style editor"
|
|
||||||
},
|
},
|
||||||
"cm_smartIndent": {
|
"cm_smartIndent": {
|
||||||
"message": "Умен отстъп",
|
"message": "Умен отстъп"
|
||||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_tabSize": {
|
"cm_tabSize": {
|
||||||
"message": "Табулация",
|
"message": "Табулация"
|
||||||
"description": "Label for the text box controlling tab size option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_theme": {
|
"cm_theme": {
|
||||||
"message": "Тема",
|
"message": "Тема"
|
||||||
"description": "Label for the style editor's CSS theme."
|
|
||||||
},
|
},
|
||||||
"confirmCancel": {
|
"confirmCancel": {
|
||||||
"message": "Отказ",
|
"message": "Отказ"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"confirmDelete": {
|
"confirmDelete": {
|
||||||
"message": "Изтриване",
|
"message": "Изтриване"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"confirmNo": {
|
"confirmNo": {
|
||||||
"message": "Не",
|
"message": "Не"
|
||||||
"description": "'No' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmOK": {
|
"confirmOK": {
|
||||||
"message": "Добре",
|
"message": "Добре"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"confirmStop": {
|
"confirmStop": {
|
||||||
"message": "Спиране",
|
"message": "Спиране"
|
||||||
"description": "'Stop' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmYes": {
|
"confirmYes": {
|
||||||
"message": "Да",
|
"message": "Да"
|
||||||
"description": "'Yes' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"dbError": {
|
"dbError": {
|
||||||
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?",
|
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?"
|
||||||
"description": "Prompt when a DB error is encountered"
|
|
||||||
},
|
},
|
||||||
"defaultTheme": {
|
"defaultTheme": {
|
||||||
"message": "по подразбиране",
|
"message": "по подразбиране"
|
||||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"deleteStyleConfirm": {
|
||||||
"message": "Сигурни ли сте, че искате да изтриете стила?",
|
"message": "Сигурни ли сте, че искате да изтриете стила?"
|
||||||
"description": "Confirmation before deleting a style"
|
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"deleteStyleLabel": {
|
||||||
"message": "Изтриване",
|
"message": "Изтриване"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове.",
|
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове."
|
||||||
"description": "Extension description"
|
|
||||||
},
|
},
|
||||||
"disableAllStyles": {
|
"disableAllStyles": {
|
||||||
"message": "Изключване на всички стилове",
|
"message": "Изключване на всички стилове"
|
||||||
"description": "Label for the checkbox that turns all enabled styles off."
|
|
||||||
},
|
},
|
||||||
"disableStyleLabel": {
|
"disableStyleLabel": {
|
||||||
"message": "Изключване",
|
"message": "Изключване"
|
||||||
"description": "Label for the button to disable a style"
|
|
||||||
},
|
},
|
||||||
"dragDropMessage": {
|
"dragDropMessage": {
|
||||||
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете.",
|
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете."
|
||||||
"description": "Drag'n'drop message"
|
|
||||||
},
|
},
|
||||||
"editDeleteText": {
|
"editDeleteText": {
|
||||||
"message": "Изтриване",
|
"message": "Изтриване"
|
||||||
"description": "Label for the context menu item in the editor to delete selected text"
|
|
||||||
},
|
},
|
||||||
"editGotoLine": {
|
"editGotoLine": {
|
||||||
"message": "Отиване на ред",
|
"message": "Отиване на ред"
|
||||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
|
||||||
},
|
},
|
||||||
"editStyleHeading": {
|
"editStyleHeading": {
|
||||||
"message": "Редактиране на стила",
|
"message": "Редактиране на стила"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "Редактиране",
|
"message": "Редактиране"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"editStyleTitle": {
|
"editStyleTitle": {
|
||||||
"message": "Редактиране на стила $stylename$",
|
"message": "Редактиране на стила $stylename$",
|
||||||
"description": "Title of the page for editing styles",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -218,116 +166,88 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"enableStyleLabel": {
|
||||||
"message": "Включване",
|
"message": "Включване"
|
||||||
"description": "Label for the button to enable a style"
|
|
||||||
},
|
},
|
||||||
"exportLabel": {
|
"exportLabel": {
|
||||||
"message": "Изнасяне",
|
"message": "Изнасяне"
|
||||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
|
||||||
},
|
},
|
||||||
"findStylesForSite": {
|
"findStylesForSite": {
|
||||||
"message": "Още стилове за този сайт",
|
"message": "Още стилове за този сайт"
|
||||||
"description": "Text for a link that gets a list of styles for the current site"
|
|
||||||
},
|
},
|
||||||
"genericDisabledLabel": {
|
"genericDisabledLabel": {
|
||||||
"message": "Изключено",
|
"message": "Изключено"
|
||||||
"description": "Used in various lists/options to indicate that something is disabled"
|
|
||||||
},
|
},
|
||||||
"genericHistoryLabel": {
|
"genericHistoryLabel": {
|
||||||
"message": "Хронология",
|
"message": "Хронология"
|
||||||
"description": "Used in various places to show a history log of something"
|
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "Помощ",
|
"message": "Помощ"
|
||||||
"description": "Alternate text for help buttons"
|
|
||||||
},
|
},
|
||||||
"helpKeyMapCommand": {
|
"helpKeyMapCommand": {
|
||||||
"message": "Въведете име",
|
"message": "Въведете име"
|
||||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
|
||||||
},
|
},
|
||||||
"helpKeyMapHotkey": {
|
"helpKeyMapHotkey": {
|
||||||
"message": "Натиснете клавиш",
|
"message": "Натиснете клавиш"
|
||||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
|
||||||
},
|
},
|
||||||
"importAppendLabel": {
|
"importAppendLabel": {
|
||||||
"message": "Прибавяне към стила",
|
"message": "Прибавяне към стила"
|
||||||
"description": "Label for the button to import a style and append to the existing sections"
|
|
||||||
},
|
},
|
||||||
"importAppendTooltip": {
|
"importAppendTooltip": {
|
||||||
"message": "Прибавяне на внесения стил към текущия",
|
"message": "Прибавяне на внесения стил към текущия"
|
||||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
|
||||||
},
|
},
|
||||||
"importLabel": {
|
"importLabel": {
|
||||||
"message": "Внасяне",
|
"message": "Внасяне"
|
||||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
|
||||||
},
|
},
|
||||||
"importReplaceLabel": {
|
"importReplaceLabel": {
|
||||||
"message": "Презаписване на стила",
|
"message": "Презаписване на стила"
|
||||||
"description": "Label for the button to import and overwrite current style"
|
|
||||||
},
|
},
|
||||||
"importReplaceTooltip": {
|
"importReplaceTooltip": {
|
||||||
"message": "Презаписване на съдържанието на текщия стил с това от внесения",
|
"message": "Презаписване на съдържанието на текщия стил с това от внесения"
|
||||||
"description": "Label for the button to import and overwrite current style"
|
|
||||||
},
|
},
|
||||||
"importReportLegendAdded": {
|
"importReportLegendAdded": {
|
||||||
"message": "добавени",
|
"message": "добавени"
|
||||||
"description": "Text after the number of styles added in the report shown after importing styles"
|
|
||||||
},
|
},
|
||||||
"importReportLegendIdentical": {
|
"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": {
|
"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": {
|
"importReportLegendUpdatedBoth": {
|
||||||
"message": "с обновени код и метаданни",
|
"message": "с обновени код и метаданни"
|
||||||
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
|
|
||||||
},
|
},
|
||||||
"importReportLegendUpdatedCode": {
|
"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": {
|
"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": {
|
"importReportTitle": {
|
||||||
"message": "Внасянето на стилове завърши",
|
"message": "Внасянето на стилове завърши"
|
||||||
"description": "Title of the report shown after importing styles"
|
|
||||||
},
|
},
|
||||||
"importReportUnchanged": {
|
"importReportUnchanged": {
|
||||||
"message": "Нищо не беше променено.",
|
"message": "Нищо не беше променено."
|
||||||
"description": "Message in the report shown after importing styles"
|
|
||||||
},
|
},
|
||||||
"importReportUndone": {
|
"importReportUndone": {
|
||||||
"message": "върнати стила",
|
"message": "върнати стила"
|
||||||
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
|
|
||||||
},
|
},
|
||||||
"importReportUndoneTitle": {
|
"importReportUndoneTitle": {
|
||||||
"message": "Внасянето беше отменено",
|
"message": "Внасянето беше отменено"
|
||||||
"description": "Title of the message box shown after undoing the import of styles"
|
|
||||||
},
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "Инсталиране на обновлението",
|
"message": "Инсталиране на обновлението"
|
||||||
"description": "Label for the button to install an update for a single style"
|
|
||||||
},
|
},
|
||||||
"linkGetHelp": {
|
"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": {
|
"linkGetStyles": {
|
||||||
"message": "Вземете стилове",
|
"message": "Вземете стилове"
|
||||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
|
||||||
},
|
},
|
||||||
"linterIssues": {
|
"linterIssues": {
|
||||||
"message": "Проблеми",
|
"message": "Проблеми"
|
||||||
"description": "Label for the CSS linter issues block on the style edit page"
|
|
||||||
},
|
},
|
||||||
"linterIssuesHelp": {
|
"linterIssuesHelp": {
|
||||||
"message": "Проблеми, намерени от $link$ при следните правила:",
|
"message": "Проблеми, намерени от $link$ при следните правила:",
|
||||||
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"link": {
|
"link": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -335,244 +255,181 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manageFavicons": {
|
"manageFavicons": {
|
||||||
"message": "Иконки на приложимите сайтовете",
|
"message": "Иконки на приложимите сайтовете"
|
||||||
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
|
|
||||||
},
|
},
|
||||||
"manageFaviconsGray": {
|
"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": {
|
"manageFaviconsHelp": {
|
||||||
"message": "Разширението използва външна услуга https://www.google.com/s2/favicons",
|
"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": {
|
"manageFilters": {
|
||||||
"message": "Филтри",
|
"message": "Филтри"
|
||||||
"description": "Label for filters container"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "Инсталирани стилове",
|
"message": "Инсталирани стилове"
|
||||||
"description": "Heading for the manage page"
|
|
||||||
},
|
},
|
||||||
"manageMaxTargets": {
|
"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": {
|
"manageNewUI": {
|
||||||
"message": "Нов интерфейс за управление",
|
"message": "Нов интерфейс за управление"
|
||||||
"description": "Label for the checkbox that toggles the new UI on manage page"
|
|
||||||
},
|
},
|
||||||
"manageOnlyEnabled": {
|
"manageOnlyEnabled": {
|
||||||
"message": "Само включените стилове",
|
"message": "Само включените стилове"
|
||||||
"description": "Checkbox to show only enabled styles"
|
|
||||||
},
|
},
|
||||||
"manageOnlyLocal": {
|
"manageOnlyLocal": {
|
||||||
"message": "Само местно създадените стилове",
|
"message": "Само местно създадените стилове"
|
||||||
"description": "Checkbox to show only locally created styles i.e. non-updatable"
|
|
||||||
},
|
},
|
||||||
"manageOnlyLocalTooltip": {
|
"manageOnlyLocalTooltip": {
|
||||||
"message": "(стиловете, които не са инсталирани през userstyles.org)",
|
"message": "(стиловете, които не са инсталирани през userstyles.org)"
|
||||||
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
|
|
||||||
},
|
},
|
||||||
"manageOnlyUpdates": {
|
"manageOnlyUpdates": {
|
||||||
"message": "Само стилове с обновления или проблеми",
|
"message": "Само стилове с обновления или проблеми"
|
||||||
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
|
|
||||||
},
|
},
|
||||||
"manageTitle": {
|
"manageTitle": {
|
||||||
"message": "Стилове",
|
"message": "Стилове"
|
||||||
"description": "Title for the manage page"
|
|
||||||
},
|
},
|
||||||
"menuShowBadge": {
|
"menuShowBadge": {
|
||||||
"message": "Брой на активните стилове",
|
"message": "Брой на активните стилове"
|
||||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
|
||||||
},
|
},
|
||||||
"noStylesForSite": {
|
"noStylesForSite": {
|
||||||
"message": "Няма инсталирани стилове за сайта.",
|
"message": "Няма инсталирани стилове за сайта."
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
|
||||||
},
|
},
|
||||||
"openManage": {
|
"openManage": {
|
||||||
"message": "Управление",
|
"message": "Управление"
|
||||||
"description": "Link to open the manage page."
|
|
||||||
},
|
},
|
||||||
"openStylesManager": {
|
"openStylesManager": {
|
||||||
"message": "Управление на стиловете",
|
"message": "Управление на стиловете"
|
||||||
"description": "Label for the style maanger opener in the browser action context menu."
|
|
||||||
},
|
},
|
||||||
"optionsActions": {
|
"optionsActions": {
|
||||||
"message": "Действия",
|
"message": "Действия"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsAdvanced": {
|
"optionsAdvanced": {
|
||||||
"message": "Разширени",
|
"message": "Разширени"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsAdvancedContextDelete": {
|
"optionsAdvancedContextDelete": {
|
||||||
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора",
|
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsAdvancedExposeIframes": {
|
"optionsAdvancedExposeIframes": {
|
||||||
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]",
|
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsBadgeDisabled": {
|
"optionsBadgeDisabled": {
|
||||||
"message": "Цвят на фона, когато е изключено",
|
"message": "Цвят на фона, когато е изключено"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsBadgeNormal": {
|
"optionsBadgeNormal": {
|
||||||
"message": "Цвят на фона",
|
"message": "Цвят на фона"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsCheck": {
|
"optionsCheck": {
|
||||||
"message": "Обновяване на стиловете",
|
"message": "Обновяване на стиловете"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsCheckUpdate": {
|
"optionsCheckUpdate": {
|
||||||
"message": "Проверка и инсталиране на наличните обновления",
|
"message": "Проверка и инсталиране на наличните обновления"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsCustomizeBadge": {
|
"optionsCustomizeBadge": {
|
||||||
"message": "Значка на иконката на лентата",
|
"message": "Значка на иконката на лентата"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsCustomizeIcon": {
|
"optionsCustomizeIcon": {
|
||||||
"message": "Иконка на лентата със сечива",
|
"message": "Иконка на лентата със сечива"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsCustomizePopup": {
|
"optionsCustomizePopup": {
|
||||||
"message": "Падащ прозорец",
|
"message": "Падащ прозорец"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsCustomizeUpdate": {
|
"optionsCustomizeUpdate": {
|
||||||
"message": "Обновления",
|
"message": "Обновления"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsHeading": {
|
"optionsHeading": {
|
||||||
"message": "Настройки",
|
"message": "Настройки"
|
||||||
"description": "Heading for options section on manage page."
|
|
||||||
},
|
},
|
||||||
"optionsIconDark": {
|
"optionsIconDark": {
|
||||||
"message": "Тъмни теми",
|
"message": "Тъмни теми"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsIconLight": {
|
"optionsIconLight": {
|
||||||
"message": "Светли теми",
|
"message": "Светли теми"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsOpen": {
|
"optionsOpen": {
|
||||||
"message": "Отваряне",
|
"message": "Отваряне"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsOpenManager": {
|
"optionsOpenManager": {
|
||||||
"message": "Управление на стиловете",
|
"message": "Управление на стиловете"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsPopupWidth": {
|
"optionsPopupWidth": {
|
||||||
"message": "Ширина на падащия прозорец (в пиксели)",
|
"message": "Ширина на падащия прозорец (в пиксели)"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsReset": {
|
"optionsReset": {
|
||||||
"message": "Зануляване на настройки на първоначалните стойности",
|
"message": "Зануляване на настройки на първоначалните стойности"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsResetButton": {
|
"optionsResetButton": {
|
||||||
"message": "Зануляване на настройките",
|
"message": "Зануляване на настройките"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsSubheading": {
|
"optionsSubheading": {
|
||||||
"message": "Още настройки",
|
"message": "Още настройки"
|
||||||
"description": "Subheading for options section on manage page."
|
|
||||||
},
|
},
|
||||||
"optionsUpdateImportNote": {
|
"optionsUpdateImportNote": {
|
||||||
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални.",
|
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални."
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"popupStylesFirst": {
|
"popupStylesFirst": {
|
||||||
"message": "Стилове преди командите",
|
"message": "Стилове преди командите"
|
||||||
"description": "Label for the checkbox controlling section order in the popup."
|
|
||||||
},
|
},
|
||||||
"prefShowBadge": {
|
"prefShowBadge": {
|
||||||
"message": "Брой на активните стилове за текущия сайт",
|
"message": "Брой на активните стилове за текущия сайт"
|
||||||
"description": "Label for the checkbox controlling toolbar badge text."
|
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
"message": "Заместване",
|
"message": "Заместване"
|
||||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
|
||||||
},
|
},
|
||||||
"replaceAll": {
|
"replaceAll": {
|
||||||
"message": "Заместване на всички",
|
"message": "Заместване на всички"
|
||||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
|
||||||
},
|
},
|
||||||
"replaceWith": {
|
"replaceWith": {
|
||||||
"message": "Заместване с",
|
"message": "Заместване с"
|
||||||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
|
||||||
},
|
},
|
||||||
"retrieveBckp": {
|
"retrieveBckp": {
|
||||||
"message": "Внасяне на стилове",
|
"message": "Внасяне на стилове"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"message": "Търсене",
|
"message": "Търсене"
|
||||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
|
||||||
},
|
},
|
||||||
"searchRegexp": {
|
"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": {
|
"sectionAdd": {
|
||||||
"message": "Добавяне на друг отдел",
|
"message": "Добавяне на друг отдел"
|
||||||
"description": "Label for the button to add a section"
|
|
||||||
},
|
},
|
||||||
"sectionCode": {
|
"sectionCode": {
|
||||||
"message": "Код",
|
"message": "Код"
|
||||||
"description": "Label for the code for a section"
|
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"sectionRemove": {
|
||||||
"message": "Премахване на отдела",
|
"message": "Премахване на отдела"
|
||||||
"description": "Label for the button to remove a section"
|
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"message": "Клавишни комбинации",
|
"message": "Клавишни комбинации"
|
||||||
"description": "Go to shortcut configuration"
|
|
||||||
},
|
},
|
||||||
"shortcutsNote": {
|
"shortcutsNote": {
|
||||||
"message": "Задаване на клавишни комбинации",
|
"message": "Задаване на клавишни комбинации"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"styleBadRegexp": {
|
"styleBadRegexp": {
|
||||||
"message": "Регулярният израз не е правилен.",
|
"message": "Регулярният израз не е правилен."
|
||||||
"description": "Validation message for a bad regexp in a style"
|
|
||||||
},
|
},
|
||||||
"styleBeautify": {
|
"styleBeautify": {
|
||||||
"message": "Разкрасяване",
|
"message": "Разкрасяване"
|
||||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
|
||||||
},
|
},
|
||||||
"styleBeautifyIndentConditional": {
|
"styleBeautifyIndentConditional": {
|
||||||
"message": "Отстъп на @media, @supports",
|
"message": "Отстъп на @media, @supports"
|
||||||
"description": "CSS-beautifier option"
|
|
||||||
},
|
},
|
||||||
"styleCancelEditLabel": {
|
"styleCancelEditLabel": {
|
||||||
"message": "Назад към стиловете",
|
"message": "Назад към стиловете"
|
||||||
"description": "Label for cancel button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleChangesNotSaved": {
|
"styleChangesNotSaved": {
|
||||||
"message": "Направили сте промени по стила без да ги запазите.",
|
"message": "Направили сте промени по стила без да ги запазите."
|
||||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
|
||||||
},
|
},
|
||||||
"styleEnabledLabel": {
|
"styleEnabledLabel": {
|
||||||
"message": "Включено",
|
"message": "Включено"
|
||||||
"description": "Label for the enabled state of styles"
|
|
||||||
},
|
},
|
||||||
"styleFromMozillaFormatPrompt": {
|
"styleFromMozillaFormatPrompt": {
|
||||||
"message": "Поставете кода във формат на Мозила",
|
"message": "Поставете кода във формат на Мозила"
|
||||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
|
||||||
},
|
},
|
||||||
"styleInstall": {
|
"styleInstall": {
|
||||||
"message": "Да се инсталира ли '$stylename$'?",
|
"message": "Да се инсталира ли '$stylename$'?",
|
||||||
"description": "Confirmation when installing a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -580,68 +437,52 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Въведете име",
|
"message": "Въведете име"
|
||||||
"description": "Error displayed when user saves without providing a name"
|
|
||||||
},
|
},
|
||||||
"styleMozillaFormatHeading": {
|
"styleMozillaFormatHeading": {
|
||||||
"message": "Формат на Мозила",
|
"message": "Формат на Мозила"
|
||||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
|
||||||
},
|
},
|
||||||
"styleNotAppliedRegexpProblemTooltip": {
|
"styleNotAppliedRegexpProblemTooltip": {
|
||||||
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази",
|
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази"
|
||||||
"description": "Tooltip in the popup for styles that were not applied at all"
|
|
||||||
},
|
},
|
||||||
"styleRegexpInvalidExplanation": {
|
"styleRegexpInvalidExplanation": {
|
||||||
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани.",
|
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани."
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"styleRegexpPartialExplanation": {
|
"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": {
|
"styleRegexpProblemTooltip": {
|
||||||
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази",
|
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази"
|
||||||
"description": "Tooltip in the popup for styles that were applied only partially"
|
|
||||||
},
|
},
|
||||||
"styleRegexpTestButton": {
|
"styleRegexpTestButton": {
|
||||||
"message": "Тест на регулярния израз",
|
"message": "Тест на регулярния израз"
|
||||||
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
|
|
||||||
},
|
},
|
||||||
"styleRegexpTestFull": {
|
"styleRegexpTestFull": {
|
||||||
"message": "Съвпадащи подпрозорци",
|
"message": "Съвпадащи подпрозорци"
|
||||||
"description": "RegExp test report: label for the fully matching expressions"
|
|
||||||
},
|
},
|
||||||
"styleRegexpTestInvalid": {
|
"styleRegexpTestInvalid": {
|
||||||
"message": "Неправилните регулярни изрази са пропуснати",
|
"message": "Неправилните регулярни изрази са пропуснати"
|
||||||
"description": "RegExp test report: label for the invalid expressions"
|
|
||||||
},
|
},
|
||||||
"styleRegexpTestNone": {
|
"styleRegexpTestNone": {
|
||||||
"message": "Няма съвпадащи подпрозорци",
|
"message": "Няма съвпадащи подпрозорци"
|
||||||
"description": "RegExp test report: label for expressions that didn't match any tabs"
|
|
||||||
},
|
},
|
||||||
"styleRegexpTestPartial": {
|
"styleRegexpTestPartial": {
|
||||||
"message": "Не съвпада напълно, затова е пропуснато",
|
"message": "Не съвпада напълно, затова е пропуснато"
|
||||||
"description": "RegExp test report: label for the partially matching expressions"
|
|
||||||
},
|
},
|
||||||
"styleRegexpTestTitle": {
|
"styleRegexpTestTitle": {
|
||||||
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)",
|
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)"
|
||||||
"description": "RegExp test report: title of the report"
|
|
||||||
},
|
},
|
||||||
"styleSaveLabel": {
|
"styleSaveLabel": {
|
||||||
"message": "Запазване",
|
"message": "Запазване"
|
||||||
"description": "Label for save button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"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": {
|
"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": {
|
"styleUpdate": {
|
||||||
"message": "Сигурни ли сте, че искате да обновите '$stylename$'?",
|
"message": "Сигурни ли сте, че искате да обновите '$stylename$'?",
|
||||||
"description": "Confirmation when updating a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -649,44 +490,34 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stylusUnavailableForURL": {
|
"stylusUnavailableForURL": {
|
||||||
"message": "Разширението не работи на такива страници.",
|
"message": "Разширението не работи на такива страници."
|
||||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
|
||||||
},
|
},
|
||||||
"stylusUnavailableForURLdetails": {
|
"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": {
|
"toggleStyle": {
|
||||||
"message": "Превключване на стила",
|
"message": "Превключване на стила"
|
||||||
"description": "Label for the checkbox to enable/disable a style"
|
|
||||||
},
|
},
|
||||||
"undo": {
|
"undo": {
|
||||||
"message": "Отмяна",
|
"message": "Отмяна"
|
||||||
"description": "Button label"
|
|
||||||
},
|
},
|
||||||
"undoGlobal": {
|
"undoGlobal": {
|
||||||
"message": "Отмяна във всички отдели",
|
"message": "Отмяна във всички отдели"
|
||||||
"description": "CSS-beautify global Undo button label"
|
|
||||||
},
|
},
|
||||||
"unreachableContentScript": {
|
"unreachableContentScript": {
|
||||||
"message": "Няма връзка със страницата. Презаредете подпрозореца.",
|
"message": "Няма връзка със страницата. Презаредете подпрозореца."
|
||||||
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
|
|
||||||
},
|
},
|
||||||
"unreachableFileHint": {
|
"unreachableFileHint": {
|
||||||
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions.",
|
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions."
|
||||||
"description": "Note in the toolbar popup for file:// URLs"
|
|
||||||
},
|
},
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
"updateAllCheckSucceededNoUpdate": {
|
||||||
"message": "Няма намерени обновления.",
|
"message": "Няма намерени обновления."
|
||||||
"description": "Text that displays when an update all check completed and no updates are available"
|
|
||||||
},
|
},
|
||||||
"updateAllCheckSucceededSomeEdited": {
|
"updateAllCheckSucceededSomeEdited": {
|
||||||
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани).",
|
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани)."
|
||||||
"description": "Text that displays when an update all check completed and no updates are available"
|
|
||||||
},
|
},
|
||||||
"updateCheckFailBadResponseCode": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "Неуспешно обновяване: сървърът отговори с код $code$.",
|
"message": "Неуспешно обновяване: сървърът отговори с код $code$.",
|
||||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"code": {
|
"code": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -694,47 +525,36 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updateCheckFailServerUnreachable": {
|
"updateCheckFailServerUnreachable": {
|
||||||
"message": "Неуспешно обновяване: няма връзка със сървъра.",
|
"message": "Неуспешно обновяване: няма връзка със сървъра."
|
||||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
|
||||||
},
|
},
|
||||||
"updateCheckHistory": {
|
"updateCheckHistory": {
|
||||||
"message": "Хронология на проверките",
|
"message": "Хронология на проверките"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"updateCheckManualUpdateForce": {
|
"updateCheckManualUpdateForce": {
|
||||||
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)",
|
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)"
|
||||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
|
||||||
},
|
},
|
||||||
"updateCheckManualUpdateHint": {
|
"updateCheckManualUpdateHint": {
|
||||||
"message": "Принудителното обновяване ще презапише местните редакции.",
|
"message": "Принудителното обновяване ще презапише местните редакции."
|
||||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
|
||||||
},
|
},
|
||||||
"updateCheckSkippedLocallyEdited": {
|
"updateCheckSkippedLocallyEdited": {
|
||||||
"message": "Стилът е бил местно редактиран.",
|
"message": "Стилът е бил местно редактиран."
|
||||||
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
|
|
||||||
},
|
},
|
||||||
"updateCheckSkippedMaybeLocallyEdited": {
|
"updateCheckSkippedMaybeLocallyEdited": {
|
||||||
"message": "Стилът може да е бил местно редактиран.",
|
"message": "Стилът може да е бил местно редактиран."
|
||||||
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
|
|
||||||
},
|
},
|
||||||
"updateCheckSucceededNoUpdate": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "Стилът е обновен.",
|
"message": "Стилът е обновен."
|
||||||
"description": "Text that displays when an update check completed and no update is available"
|
|
||||||
},
|
},
|
||||||
"updateCompleted": {
|
"updateCompleted": {
|
||||||
"message": "Обновяването е завършено.",
|
"message": "Обновяването е завършено."
|
||||||
"description": "Text that displays when an update completed"
|
|
||||||
},
|
},
|
||||||
"updatesCurrentlyInstalled": {
|
"updatesCurrentlyInstalled": {
|
||||||
"message": "Инсталирани обновления:",
|
"message": "Инсталирани обновления:"
|
||||||
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
|
||||||
},
|
},
|
||||||
"writeStyleFor": {
|
"writeStyleFor": {
|
||||||
"message": "Писане на стил за: ",
|
"message": "Писане на стил за: "
|
||||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
|
||||||
},
|
},
|
||||||
"writeStyleForURL": {
|
"writeStyleForURL": {
|
||||||
"message": "този адрес",
|
"message": "този адрес"
|
||||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Напиши нов стил",
|
"message": "Напиши нов стил"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Добави стил",
|
"message": "Добави стил"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Добави",
|
"message": "Добави"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Прилага се към: $applies$",
|
"message": "Прилага се към: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,136 +17,103 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "и още",
|
"message": "и още"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "URLи на домейна",
|
"message": "URLи на домейна"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesHelp": {
|
"appliesHelp": {
|
||||||
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция.",
|
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция."
|
||||||
"description": "Help text for 'applies to' section"
|
|
||||||
},
|
},
|
||||||
"appliesLabel": {
|
"appliesLabel": {
|
||||||
"message": "Прилага се към",
|
"message": "Прилага се към"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"appliesRegexpOption": {
|
||||||
"message": "Адреси, съвпадащи с regexp",
|
"message": "Адреси, съвпадащи с regexp"
|
||||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"appliesRemove": {
|
||||||
"message": "Премахни",
|
"message": "Премахни"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Уточни",
|
"message": "Уточни"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Всички",
|
"message": "Всички"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"appliesUrlPrefixOption": {
|
||||||
"message": "URL започващи с",
|
"message": "URL започващи с"
|
||||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
|
||||||
},
|
},
|
||||||
"applyAllUpdates": {
|
"applyAllUpdates": {
|
||||||
"message": "Приложи всички промени",
|
"message": "Приложи всички промени"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"checkAllUpdates": {
|
||||||
"message": "Провери всички стилове за обновления",
|
"message": "Провери всички стилове за обновления"
|
||||||
"description": "Label for the button to check all styles for updates"
|
|
||||||
},
|
},
|
||||||
"checkForUpdate": {
|
"checkForUpdate": {
|
||||||
"message": "Провери за обновление",
|
"message": "Провери за обновление"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Проверявам...",
|
"message": "Проверявам..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"cm_indentWithTabs": {
|
"cm_indentWithTabs": {
|
||||||
"message": "Използвай табулация с умно отместване",
|
"message": "Използвай табулация с умно отместване"
|
||||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_keyMap": {
|
"cm_keyMap": {
|
||||||
"message": "Клавишни комбинации",
|
"message": "Клавишни комбинации"
|
||||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_lineWrapping": {
|
"cm_lineWrapping": {
|
||||||
"message": "Автоматично пренасяне",
|
"message": "Автоматично пренасяне"
|
||||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_smartIndent": {
|
"cm_smartIndent": {
|
||||||
"message": "Използвай умно отместване",
|
"message": "Използвай умно отместване"
|
||||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_tabSize": {
|
"cm_tabSize": {
|
||||||
"message": "Размер на табулацията",
|
"message": "Размер на табулацията"
|
||||||
"description": "Label for the text box controlling tab size option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_theme": {
|
"cm_theme": {
|
||||||
"message": "Тема",
|
"message": "Тема"
|
||||||
"description": "Label for the style editor's CSS theme."
|
|
||||||
},
|
},
|
||||||
"confirmNo": {
|
"confirmNo": {
|
||||||
"message": "Не",
|
"message": "Не"
|
||||||
"description": "'No' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmStop": {
|
"confirmStop": {
|
||||||
"message": "Спри",
|
"message": "Спри"
|
||||||
"description": "'Stop' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmYes": {
|
"confirmYes": {
|
||||||
"message": "Да",
|
"message": "Да"
|
||||||
"description": "'Yes' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"dbError": {
|
"dbError": {
|
||||||
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?",
|
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?"
|
||||||
"description": "Prompt when a DB error is encountered"
|
|
||||||
},
|
},
|
||||||
"defaultTheme": {
|
"defaultTheme": {
|
||||||
"message": "по подразбиране",
|
"message": "по подразбиране"
|
||||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"deleteStyleConfirm": {
|
||||||
"message": "Наистина ли искаш да изтриеш този стил?",
|
"message": "Наистина ли искаш да изтриеш този стил?"
|
||||||
"description": "Confirmation before deleting a style"
|
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"deleteStyleLabel": {
|
||||||
"message": "Изтрий",
|
"message": "Изтрий"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове.",
|
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове."
|
||||||
"description": "Extension description"
|
|
||||||
},
|
},
|
||||||
"disableAllStyles": {
|
"disableAllStyles": {
|
||||||
"message": "Изключи всички стилове",
|
"message": "Изключи всички стилове"
|
||||||
"description": "Label for the checkbox that turns all enabled styles off."
|
|
||||||
},
|
},
|
||||||
"disableStyleLabel": {
|
"disableStyleLabel": {
|
||||||
"message": "Забрани",
|
"message": "Забрани"
|
||||||
"description": "Label for the button to disable a style"
|
|
||||||
},
|
},
|
||||||
"editGotoLine": {
|
"editGotoLine": {
|
||||||
"message": "Иди на ред (или ред:кол)",
|
"message": "Иди на ред (или ред:кол)"
|
||||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
|
||||||
},
|
},
|
||||||
"editStyleHeading": {
|
"editStyleHeading": {
|
||||||
"message": "Промени стила",
|
"message": "Промени стила"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "Редактирай",
|
"message": "Редактирай"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"editStyleTitle": {
|
"editStyleTitle": {
|
||||||
"message": "Редактирай стил $stylename$",
|
"message": "Редактирай стил $stylename$",
|
||||||
"description": "Title of the page for editing styles",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -158,68 +121,52 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"enableStyleLabel": {
|
||||||
"message": "Разреши",
|
"message": "Разреши"
|
||||||
"description": "Label for the button to enable a style"
|
|
||||||
},
|
},
|
||||||
"exportLabel": {
|
"exportLabel": {
|
||||||
"message": "Експорт",
|
"message": "Експорт"
|
||||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "Помощ",
|
"message": "Помощ"
|
||||||
"description": "Alternate text for help buttons"
|
|
||||||
},
|
},
|
||||||
"helpKeyMapCommand": {
|
"helpKeyMapCommand": {
|
||||||
"message": "Напиши име на команда",
|
"message": "Напиши име на команда"
|
||||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
|
||||||
},
|
},
|
||||||
"helpKeyMapHotkey": {
|
"helpKeyMapHotkey": {
|
||||||
"message": "Натисни клавишна комбинация",
|
"message": "Натисни клавишна комбинация"
|
||||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
|
||||||
},
|
},
|
||||||
"importAppendLabel": {
|
"importAppendLabel": {
|
||||||
"message": "Добави към стил",
|
"message": "Добави към стил"
|
||||||
"description": "Label for the button to import a style and append to the existing sections"
|
|
||||||
},
|
},
|
||||||
"importAppendTooltip": {
|
"importAppendTooltip": {
|
||||||
"message": "Добави импортирания стил към текущия",
|
"message": "Добави импортирания стил към текущия"
|
||||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
|
||||||
},
|
},
|
||||||
"importLabel": {
|
"importLabel": {
|
||||||
"message": "Импорт",
|
"message": "Импорт"
|
||||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
|
||||||
},
|
},
|
||||||
"importReplaceLabel": {
|
"importReplaceLabel": {
|
||||||
"message": "Презапиши стила",
|
"message": "Презапиши стила"
|
||||||
"description": "Label for the button to import and overwrite current style"
|
|
||||||
},
|
},
|
||||||
"importReplaceTooltip": {
|
"importReplaceTooltip": {
|
||||||
"message": "Презапишете съдържанието на текущия стил с импортирания",
|
"message": "Презапишете съдържанието на текущия стил с импортирания"
|
||||||
"description": "Label for the button to import and overwrite current style"
|
|
||||||
},
|
},
|
||||||
"installButton": {
|
"installButton": {
|
||||||
"message": "Инсталирай стил",
|
"message": "Инсталирай стил"
|
||||||
"description": "Label for install button"
|
|
||||||
},
|
},
|
||||||
"installButtonInstalled": {
|
"installButtonInstalled": {
|
||||||
"message": "Стилът е инсталиран",
|
"message": "Стилът е инсталиран"
|
||||||
"description": "Text displayed when the style is successfully installed"
|
|
||||||
},
|
},
|
||||||
"installButtonReinstall": {
|
"installButtonReinstall": {
|
||||||
"message": "Преинсталирай стила",
|
"message": "Преинсталирай стила"
|
||||||
"description": "Label for reinstall button"
|
|
||||||
},
|
},
|
||||||
"installButtonUpdate": {
|
"installButtonUpdate": {
|
||||||
"message": "Обнови стила",
|
"message": "Обнови стила"
|
||||||
"description": "Label for update button"
|
|
||||||
},
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "Инсталирай обновление",
|
"message": "Инсталирай обновление"
|
||||||
"description": "Label for the button to install an update for a single style"
|
|
||||||
},
|
},
|
||||||
"installUpdateFrom": {
|
"installUpdateFrom": {
|
||||||
"message": "В момента стилът се обновява от $url$",
|
"message": "В момента стилът се обновява от $url$",
|
||||||
"description": "Label to describe where the style gets update",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"url": {
|
"url": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -227,28 +174,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installUpdateFromLabel": {
|
"installUpdateFromLabel": {
|
||||||
"message": "Провери за обновления",
|
"message": "Провери за обновления"
|
||||||
"description": "Label for the checkbox to save current URL for update check"
|
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"message": "Лиценз",
|
"message": "Лиценз"
|
||||||
"description": "Label for the license"
|
|
||||||
},
|
},
|
||||||
"linkGetHelp": {
|
"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": {
|
"linkGetStyles": {
|
||||||
"message": "Вземете стилове",
|
"message": "Вземете стилове"
|
||||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
|
||||||
},
|
},
|
||||||
"linkTranslate": {
|
"linkTranslate": {
|
||||||
"message": "Преведете",
|
"message": "Преведете"
|
||||||
"description": "Transifex link text on the manage page"
|
|
||||||
},
|
},
|
||||||
"linterCSSLintIncompatible": {
|
"linterCSSLintIncompatible": {
|
||||||
"message": "CSSLint не поддържа $preprocessorname$ preprocessor",
|
"message": "CSSLint не поддържа $preprocessorname$ preprocessor",
|
||||||
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"preprocessorname": {
|
"preprocessorname": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -256,12 +197,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linterCSSLintSettings": {
|
"linterCSSLintSettings": {
|
||||||
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)",
|
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)"
|
||||||
"description": "CSSLint rule config values"
|
|
||||||
},
|
},
|
||||||
"linterConfigPopupTitle": {
|
"linterConfigPopupTitle": {
|
||||||
"message": "Настройте конфигурация за $linter$ правила",
|
"message": "Настройте конфигурация за $linter$ правила",
|
||||||
"description": "Stylelint or CSSLint popup header",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"linter": {
|
"linter": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -269,20 +208,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linterConfigTooltip": {
|
"linterConfigTooltip": {
|
||||||
"message": "Щракнете, за да конфигурирате този linter",
|
"message": "Щракнете, за да конфигурирате този linter"
|
||||||
"description": "Icon tooltip to indicate that it opens a popup with the selected linter configuration"
|
|
||||||
},
|
},
|
||||||
"linterInvalidConfigError": {
|
"linterInvalidConfigError": {
|
||||||
"message": "Не е записано заради тези неправилни настройки",
|
"message": "Не е записано заради тези неправилни настройки"
|
||||||
"description": "Invalid linter config will show a message followed by a list of invalid entries"
|
|
||||||
},
|
},
|
||||||
"linterIssues": {
|
"linterIssues": {
|
||||||
"message": "Проблеми",
|
"message": "Проблеми"
|
||||||
"description": "Label for the CSS linter issues block on the style edit page"
|
|
||||||
},
|
},
|
||||||
"linterIssuesHelp": {
|
"linterIssuesHelp": {
|
||||||
"message": "Тези проблеми бяха намерени от $link$:",
|
"message": "Тези проблеми бяха намерени от $link$:",
|
||||||
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"link": {
|
"link": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -290,71 +225,54 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"linterJSONError": {
|
"linterJSONError": {
|
||||||
"message": "Невалиден JSON формат",
|
"message": "Невалиден JSON формат"
|
||||||
"description": "Setting linter config with invalid JSON"
|
|
||||||
},
|
},
|
||||||
"linterResetMessage": {
|
"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": {
|
"linterRulesLink": {
|
||||||
"message": "Вижте пълния списък с правила",
|
"message": "Вижте пълния списък с правила"
|
||||||
"description": "Stylelint or CSSLint rules label added immediately before a link"
|
|
||||||
},
|
},
|
||||||
"liveReloadError": {
|
"liveReloadError": {
|
||||||
"message": "Получи се грешка докато наблюдавахме файла",
|
"message": "Получи се грешка докато наблюдавахме файла"
|
||||||
"description": "The label of live-reload error"
|
|
||||||
},
|
},
|
||||||
"liveReloadLabel": {
|
"liveReloadLabel": {
|
||||||
"message": "Преглед на живо",
|
"message": "Преглед на живо"
|
||||||
"description": "The label of live-reload feature"
|
|
||||||
},
|
},
|
||||||
"manageFilters": {
|
"manageFilters": {
|
||||||
"message": "Филтри",
|
"message": "Филтри"
|
||||||
"description": "Label for filters container"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "Инсталирани стилове",
|
"message": "Инсталирани стилове"
|
||||||
"description": "Heading for the manage page"
|
|
||||||
},
|
},
|
||||||
"manageNewStyleAsUsercss": {
|
"manageNewStyleAsUsercss": {
|
||||||
"message": "като Потребителскиcss",
|
"message": "като Потребителскиcss"
|
||||||
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
|
|
||||||
},
|
},
|
||||||
"manageNewUI": {
|
"manageNewUI": {
|
||||||
"message": "Нова подредба на UI",
|
"message": "Нова подредба на UI"
|
||||||
"description": "Label for the checkbox that toggles the new UI on manage page"
|
|
||||||
},
|
},
|
||||||
"manageOnlyDisabled": {
|
"manageOnlyDisabled": {
|
||||||
"message": "Само забранените стилове",
|
"message": "Само забранените стилове"
|
||||||
"description": "Checkbox to show only disabled styles"
|
|
||||||
},
|
},
|
||||||
"manageOnlyEnabled": {
|
"manageOnlyEnabled": {
|
||||||
"message": "Само разрешените стилове",
|
"message": "Само разрешените стилове"
|
||||||
"description": "Checkbox to show only enabled styles"
|
|
||||||
},
|
},
|
||||||
"manageOnlyExternal": {
|
"manageOnlyExternal": {
|
||||||
"message": "Само външните стилове",
|
"message": "Само външните стилове"
|
||||||
"description": "Checkbox to show only externally installed styles i.e. updatable"
|
|
||||||
},
|
},
|
||||||
"manageOnlyLocal": {
|
"manageOnlyLocal": {
|
||||||
"message": "Само локалните стилове",
|
"message": "Само локалните стилове"
|
||||||
"description": "Checkbox to show only locally created styles i.e. non-updatable"
|
|
||||||
},
|
},
|
||||||
"manageOnlyLocalTooltip": {
|
"manageOnlyLocalTooltip": {
|
||||||
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)",
|
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)"
|
||||||
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
|
|
||||||
},
|
},
|
||||||
"manageOnlyNonUsercss": {
|
"manageOnlyNonUsercss": {
|
||||||
"message": "Само не-Потребителскитеcss стилове",
|
"message": "Само не-Потребителскитеcss стилове"
|
||||||
"description": "Checkbox to show only non-Usercss (standard) styles"
|
|
||||||
},
|
},
|
||||||
"manageOnlyUpdates": {
|
"manageOnlyUpdates": {
|
||||||
"message": "Само с обновления или проблеми",
|
"message": "Само с обновления или проблеми"
|
||||||
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
|
|
||||||
},
|
},
|
||||||
"manageOnlyUsercss": {
|
"manageOnlyUsercss": {
|
||||||
"message": "Само Потребителскиcss стилове",
|
"message": "Само Потребителскиcss стилове"
|
||||||
"description": "Checkbox to show only Usercss styles"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,23 +1,18 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Skriv ny stil",
|
"message": "Skriv ny stil"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Tilføj stil",
|
"message": "Tilføj stil"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"alphaChannel": {
|
"alphaChannel": {
|
||||||
"message": "Gennemsigtighed",
|
"message": "Gennemsigtighed"
|
||||||
"description": "Label of color's opacity"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Tilføj",
|
"message": "Tilføj"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Anvendes på: $applies$",
|
"message": "Anvendes på: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -25,107 +20,81 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "og mere",
|
"message": "og mere"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"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": {
|
"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": {
|
"appliesLabel": {
|
||||||
"message": "Anvendes på",
|
"message": "Anvendes på"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesLineWidgetLabel": {
|
"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": {
|
"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": {
|
"appliesRemove": {
|
||||||
"message": "Fjern",
|
"message": "Fjern"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesRemoveError": {
|
"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": {
|
"appliesSpecify": {
|
||||||
"message": "Specificér",
|
"message": "Specificér"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Alt",
|
"message": "Alt"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"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": {
|
"applyAllUpdates": {
|
||||||
"message": "Anvend alle opdateringer",
|
"message": "Anvend alle opdateringer"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"message": "Forfatter",
|
"message": "Forfatter"
|
||||||
"description": "Label for the style author"
|
|
||||||
},
|
},
|
||||||
"backupMessage": {
|
"backupMessage": {
|
||||||
"message": "Vælg en fil eller træk og slip til denne side.",
|
"message": "Vælg en fil eller træk og slip til denne side."
|
||||||
"description": "Message for backup"
|
|
||||||
},
|
},
|
||||||
"bckpInstStyles": {
|
"bckpInstStyles": {
|
||||||
"message": "Eksportér stil",
|
"message": "Eksportér stil"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"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": {
|
"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": {
|
"checkForUpdate": {
|
||||||
"message": "Tjek efter opdatering",
|
"message": "Tjek efter opdatering"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Tjekker...",
|
"message": "Tjekker..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"clickToUninstall": {
|
"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": {
|
"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": {
|
"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": {
|
"cm_autocompleteOnTyping": {
|
||||||
"message": "Autoudfyld på indtastning",
|
"message": "Autoudfyld på indtastning"
|
||||||
"description": "Label for the checkbox in the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_colorpicker": {
|
"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": {
|
"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": {
|
"cm_keyMap": {
|
||||||
"message": "Tastegenveje",
|
"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,19 +1,21 @@
|
||||||
{
|
{
|
||||||
|
"InaccessibleFileHint": {
|
||||||
|
"message": "Το Stylus δεν έχει πρόσβαση σε κάποια αρχεία (π.χ. τα αρχεία PDF και JSON)"
|
||||||
|
},
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Γράψτε νέο στυλ",
|
"message": "Γράψτε νέο στυλ"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Προσθήκη στυλ",
|
"message": "Προσθήκη στυλ"
|
||||||
"description": "Title of the page for adding styles"
|
},
|
||||||
|
"alphaChannel": {
|
||||||
|
"message": "Αδιαφάνεια"
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Προσθήκη",
|
"message": "Προσθήκη"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Ισχύει για: $applies$",
|
"message": "Ισχύει για: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,112 +23,231 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "και πολλά άλλα",
|
"message": "και πολλά άλλα"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "URL στον τομέα",
|
"message": "URL στον τομέα"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesHelp": {
|
"appliesHelp": {
|
||||||
"message": "Χρησιμοποιήστε το \"Ισχύει για\" έλεγχοι ώστε να περιοριστουν ποιες διευθύνσεις τον κώδικα σε αυτό το τμήμα να εφαρμόζονται.",
|
"message": "Χρησιμοποιήστε το \"Ισχύει για\" έλεγχοι ώστε να περιοριστουν ποιες διευθύνσεις τον κώδικα σε αυτό το τμήμα να εφαρμόζονται."
|
||||||
"description": "Help text for 'applies to' section"
|
|
||||||
},
|
},
|
||||||
"appliesLabel": {
|
"appliesLabel": {
|
||||||
"message": "Ισχύει για",
|
"message": "Ισχύει για"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
},
|
||||||
|
"appliesLineWidgetWarning": {
|
||||||
|
"message": "Δε λειτουργεί με minified CSS."
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"appliesRegexpOption": {
|
||||||
"message": "Διευθύνσεις URL που ταιριάζουν με την κανονική έκφραση",
|
"message": "Διευθύνσεις URL που ταιριάζουν με την κανονική έκφραση"
|
||||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"appliesRemove": {
|
||||||
"message": "Αφαίρεση",
|
"message": "Αφαίρεση"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Καθορισμός",
|
"message": "Καθορισμός"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Τα πάντα",
|
"message": "Τα πάντα"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
},
|
||||||
|
"appliesUrlOption": {
|
||||||
|
"message": "διεύθυνση URL"
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"appliesUrlPrefixOption": {
|
||||||
"message": "Διευθύνσεις URL που αρχίζουν με",
|
"message": "Διευθύνσεις URL που αρχίζουν με"
|
||||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
|
||||||
},
|
},
|
||||||
"applyAllUpdates": {
|
"applyAllUpdates": {
|
||||||
"message": "Εφαρμογή όλων των ενημερώσεων",
|
"message": "Εφαρμογή όλων των ενημερώσεων"
|
||||||
"description": "Label for the button to apply all detected updates"
|
},
|
||||||
|
"author": {
|
||||||
|
"message": "Συντάκτης"
|
||||||
|
},
|
||||||
|
"backupButtons": {
|
||||||
|
"message": "Δημιουργήστε αντίγραφο ασφαλείας"
|
||||||
|
},
|
||||||
|
"backupMessage": {
|
||||||
|
"message": "Επιλέξτε ένα αρχείο ή σύρετέ το σε αυτήν τη σελίδα"
|
||||||
|
},
|
||||||
|
"bckpInstStyles": {
|
||||||
|
"message": "Εξαγωγή στυλ"
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"checkAllUpdates": {
|
||||||
"message": "Έλεγχος όλων των στυλ για ενημερώσεις",
|
"message": "Έλεγχος όλων των στυλ για ενημερώσεις"
|
||||||
"description": "Label for the button to check all styles for updates"
|
},
|
||||||
|
"checkAllUpdatesForce": {
|
||||||
|
"message": "Ελέγξτε πάλι, δεν επεξεργάστηκα κανένα στυλ!"
|
||||||
},
|
},
|
||||||
"checkForUpdate": {
|
"checkForUpdate": {
|
||||||
"message": "Έλεγχος για ενημερώσεις",
|
"message": "Έλεγχος για ενημερώσεις"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Έλεγχος...",
|
"message": "Έλεγχος..."
|
||||||
"description": "Text to display when checking a style for an update"
|
},
|
||||||
|
"clickToUninstall": {
|
||||||
|
"message": "Πατήστε για απεγκατάσταση"
|
||||||
|
},
|
||||||
|
"cm_autoCloseBrackets": {
|
||||||
|
"message": "Αυτόματο κλείσιμο παρενθέσεων και εισαγωγικών"
|
||||||
|
},
|
||||||
|
"cm_autocompleteOnTyping": {
|
||||||
|
"message": "Αυτόματη συμπλήρωση καθώς πληκτρολογείτε"
|
||||||
},
|
},
|
||||||
"cm_indentWithTabs": {
|
"cm_indentWithTabs": {
|
||||||
"message": "Χρήση καρτελών με έξυπνη εσοχή",
|
"message": "Χρήση καρτελών με έξυπνη εσοχή"
|
||||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
},
|
||||||
|
"cm_keyMap": {
|
||||||
|
"message": "Συντομεύσεις πληκτρολογίου"
|
||||||
},
|
},
|
||||||
"cm_lineWrapping": {
|
"cm_lineWrapping": {
|
||||||
"message": "Αναδίπλωση λέξεων",
|
"message": "Αναδίπλωση λέξεων"
|
||||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
},
|
||||||
|
"cm_matchHighlight": {
|
||||||
|
"message": "Υπογράμμιση"
|
||||||
|
},
|
||||||
|
"cm_matchHighlightSelection": {
|
||||||
|
"message": "Μόνο επιλογή"
|
||||||
|
},
|
||||||
|
"cm_resizeGripHint": {
|
||||||
|
"message": "Διπλό κλικ για μεγιστοποίηση/επαναφορά ύψους"
|
||||||
},
|
},
|
||||||
"cm_smartIndent": {
|
"cm_smartIndent": {
|
||||||
"message": "Χρήση έξυπνης εσοχής",
|
"message": "Χρήση έξυπνης εσοχής"
|
||||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_tabSize": {
|
"cm_tabSize": {
|
||||||
"message": "Μέγεθος καρτέλας",
|
"message": "Μέγεθος καρτέλας"
|
||||||
"description": "Label for the text box controlling tab size option for the style editor."
|
},
|
||||||
|
"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": "Ημερομηνία ενημέρωσης"
|
||||||
},
|
},
|
||||||
"dbError": {
|
"dbError": {
|
||||||
"message": "Παρουσιάστηκε σφάλμα χρησιμοποιώντας την κομψή βάση δεδομένων. Θα θέλατε να επισκεφθείτε μια ιστοσελίδα με πιθανές λύσεις;",
|
"message": "Παρουσιάστηκε σφάλμα χρησιμοποιώντας την κομψή βάση δεδομένων. Θα θέλατε να επισκεφθείτε μια ιστοσελίδα με πιθανές λύσεις;"
|
||||||
"description": "Prompt when a DB error is encountered"
|
},
|
||||||
|
"defaultTheme": {
|
||||||
|
"message": "προεπιλογή"
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"deleteStyleConfirm": {
|
||||||
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;",
|
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;"
|
||||||
"description": "Confirmation before deleting a style"
|
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"deleteStyleLabel": {
|
||||||
"message": "Διαγραφή",
|
"message": "Διαγραφή"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Επαναπροσδιορίση του διαδίκτυου με το Stylus, έναν διαχειριστή στυλ. Το Stylus σας επιτρέπει να εγκαταστήσετε εύκολα themes και skins για πολλές δημοφιλείς ιστοσελίδες.",
|
"message": "Επαναπροσδιορίση του διαδίκτυου με το Stylus, έναν διαχειριστή στυλ. Το Stylus σας επιτρέπει να εγκαταστήσετε εύκολα themes και skins για πολλές δημοφιλείς ιστοσελίδες."
|
||||||
"description": "Extension description"
|
|
||||||
},
|
},
|
||||||
"disableAllStyles": {
|
"disableAllStyles": {
|
||||||
"message": "Απενεργοποιηση ολων των στυλ",
|
"message": "Απενεργοποιηση ολων των στυλ"
|
||||||
"description": "Label for the checkbox that turns all enabled styles off."
|
|
||||||
},
|
},
|
||||||
"disableStyleLabel": {
|
"disableStyleLabel": {
|
||||||
"message": "Απενεργοποίηση",
|
"message": "Απενεργοποίηση"
|
||||||
"description": "Label for the button to disable a style"
|
},
|
||||||
|
"dragDropMessage": {
|
||||||
|
"message": "Αποθέστε το αντίγραφο ασφαλείας σας οπουδήποτε σε αυτήν τη σελίδα για εισαγωγή."
|
||||||
|
},
|
||||||
|
"dragDropUsercssTabstrip": {
|
||||||
|
"message": "Για να εγκαταστήσετε το αρχείο, αποθέστε το στη λωρίδα καρτελών (την περιοχή όπου εμφανίζονται οι τίτλοι καρτελών)."
|
||||||
|
},
|
||||||
|
"editDeleteText": {
|
||||||
|
"message": "Διαγραφή"
|
||||||
},
|
},
|
||||||
"editGotoLine": {
|
"editGotoLine": {
|
||||||
"message": "Μετάβαση στη γραμμή (ή line:col)",
|
"message": "Μετάβαση στη γραμμή (ή line:col)"
|
||||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
|
||||||
},
|
},
|
||||||
"editStyleHeading": {
|
"editStyleHeading": {
|
||||||
"message": "Επεξεργασία Στυλ",
|
"message": "Επεξεργασία Στυλ"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "Επεξεργασία",
|
"message": "Επεξεργασία"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"editStyleTitle": {
|
"editStyleTitle": {
|
||||||
"message": "Επεξεργασία του στυλ $stylename$",
|
"message": "Επεξεργασία του στυλ $stylename$",
|
||||||
"description": "Title of the page for editing styles",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -134,92 +255,393 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"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": "Εύρεση στυλ"
|
||||||
},
|
},
|
||||||
"findStylesForSite": {
|
"findStylesForSite": {
|
||||||
"message": "Αναζήτηση περισσότερων στυλ για αυτή την ιστοσελίδα",
|
"message": "Αναζήτηση περισσότερων στυλ για αυτή την ιστοσελίδα"
|
||||||
"description": "Text for a link that gets a list of styles for the current site"
|
},
|
||||||
|
"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": "Λήψη όλων των στυλ..."
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "Βοήθεια",
|
"message": "Βοήθεια"
|
||||||
"description": "Alternate text for help buttons"
|
},
|
||||||
|
"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": "Ενημέρωση στυλ"
|
||||||
},
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "Εγκατάσταση ενημέρωσης",
|
"message": "Εγκατάσταση ενημέρωσης"
|
||||||
"description": "Label for the button to install an update for a single style"
|
},
|
||||||
|
"installUpdateFromLabel": {
|
||||||
|
"message": "Έλεγχος για ενημερώσεις"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"message": "Άδεια χρήσης"
|
||||||
|
},
|
||||||
|
"linkGetHelp": {
|
||||||
|
"message": "Βοήθεια"
|
||||||
|
},
|
||||||
|
"linkGetStyles": {
|
||||||
|
"message": "Λήψη στυλ"
|
||||||
|
},
|
||||||
|
"linkTranslate": {
|
||||||
|
"message": "Μετάφραση"
|
||||||
|
},
|
||||||
|
"linterConfigTooltip": {
|
||||||
|
"message": "Πατήστε εδώ για να ρυθμίσετε το linter"
|
||||||
|
},
|
||||||
|
"linterIssues": {
|
||||||
|
"message": "Ζητήματα"
|
||||||
|
},
|
||||||
|
"linterJSONError": {
|
||||||
|
"message": "Μη έγκυρη μορφή JSON"
|
||||||
|
},
|
||||||
|
"linterResetMessage": {
|
||||||
|
"message": "Για αναίρεση μιας κατά λάθος επαναφοράς, πατήστε Ctrl-Z (ή Cmd-Z) στο πλαίσιο κειμένου"
|
||||||
|
},
|
||||||
|
"manageFaviconsHelp": {
|
||||||
|
"message": "Το Stylus χρησιμοποιεί μία εξωτερική υπηρεσία https://www.google.com/s2/favicons"
|
||||||
},
|
},
|
||||||
"manageFilters": {
|
"manageFilters": {
|
||||||
"message": "Φίλτρα",
|
"message": "Φίλτρα"
|
||||||
"description": "Label for filters container"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "Εγκατεστημένα Στυλ",
|
"message": "Εγκατεστημένα Στυλ"
|
||||||
"description": "Heading for the manage page"
|
},
|
||||||
|
"manageNewUI": {
|
||||||
|
"message": "Νέα διαχείριση διάταξης UI"
|
||||||
|
},
|
||||||
|
"manageOnlyDisabled": {
|
||||||
|
"message": "Μόνο απενεργοποιημένα στυλ"
|
||||||
},
|
},
|
||||||
"manageOnlyEnabled": {
|
"manageOnlyEnabled": {
|
||||||
"message": "Μόνο ενεργοποιημένα στυλ",
|
"message": "Μόνο ενεργοποιημένα στυλ"
|
||||||
"description": "Checkbox to show only enabled styles"
|
},
|
||||||
|
"manageOnlyExternal": {
|
||||||
|
"message": "Μόνο στυλ από άλλες ιστοσελίδες"
|
||||||
|
},
|
||||||
|
"manageOnlyLocal": {
|
||||||
|
"message": "Μόνο στυλ δημιουργημένα τοπικά"
|
||||||
},
|
},
|
||||||
"manageTitle": {
|
"manageTitle": {
|
||||||
"message": "Κομψή",
|
"message": "Κομψή"
|
||||||
"description": "Title for the manage page"
|
|
||||||
},
|
},
|
||||||
"menuShowBadge": {
|
"menuShowBadge": {
|
||||||
"message": "Εμφάνιση ενεργους καταμέτρησης στυλ",
|
"message": "Εμφάνιση ενεργους καταμέτρησης στυλ"
|
||||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
},
|
||||||
|
"noFileToImport": {
|
||||||
|
"message": "Για να εισάγετε τα στυλ σας, πρέπει πρώτα να τα εξάγετε."
|
||||||
},
|
},
|
||||||
"noStylesForSite": {
|
"noStylesForSite": {
|
||||||
"message": "Δεν υπάρχουν εγκατεστημένα στυλ για αυτή την ιστοσελίδα.",
|
"message": "Δεν υπάρχουν εγκατεστημένα στυλ για αυτή την ιστοσελίδα."
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
|
||||||
},
|
},
|
||||||
"openManage": {
|
"openManage": {
|
||||||
"message": "Διαχείριση εγκατεστημένων στυλ",
|
"message": "Διαχείριση εγκατεστημένων στυλ"
|
||||||
"description": "Link to open the manage page."
|
},
|
||||||
|
"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": "Ενημερώσεις"
|
||||||
},
|
},
|
||||||
"optionsHeading": {
|
"optionsHeading": {
|
||||||
"message": "Επιλογές",
|
"message": "Επιλογές"
|
||||||
"description": "Heading for options section on manage page."
|
},
|
||||||
|
"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": "Συγχρονισμός τώρα"
|
||||||
|
},
|
||||||
|
"optionsUpdateInterval": {
|
||||||
|
"message": "Διάστημα αυτόματης ενημέρωσης των στυλ σε ώρες (0 για απενεργοποίηση)"
|
||||||
|
},
|
||||||
|
"paginationNext": {
|
||||||
|
"message": "Επόμενη σελίδα"
|
||||||
|
},
|
||||||
|
"paginationPrevious": {
|
||||||
|
"message": "Προηγούμενη σελίδα"
|
||||||
|
},
|
||||||
|
"popupBordersTooltip": {
|
||||||
|
"message": "Χρήσιμο για σκούρα θέματα στο καινούριο Chrome, καθώς δε βάφει πλέον τα ακριανά περιθώρια."
|
||||||
|
},
|
||||||
|
"popupOpenEditInPopup": {
|
||||||
|
"message": "Χρήση ενός απλού παραθύρου (χωρίς omnibox)"
|
||||||
|
},
|
||||||
|
"popupOpenEditInWindow": {
|
||||||
|
"message": "Άνοιγμα επεξαργαστή σε νέο παράθυρο"
|
||||||
},
|
},
|
||||||
"popupStylesFirst": {
|
"popupStylesFirst": {
|
||||||
"message": "Στυλ λίστας πριν των εντολών στο μενού του κουμπιού γραμμής εργαλείων",
|
"message": "Στυλ λίστας πριν των εντολών στο μενού του κουμπιού γραμμής εργαλείων"
|
||||||
"description": "Label for the checkbox controlling section order in the popup."
|
|
||||||
},
|
},
|
||||||
"prefShowBadge": {
|
"prefShowBadge": {
|
||||||
"message": "Εμφάνιση αριθμού των στυλ που δραστηριοποιούνται για την τρέχουσα τοποθεσία στην μπάρα εργαλείων",
|
"message": "Εμφάνιση αριθμού των στυλ που δραστηριοποιούνται για την τρέχουσα τοποθεσία στην μπάρα εργαλείων"
|
||||||
"description": "Label for the checkbox controlling toolbar badge text."
|
},
|
||||||
|
"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": "Όνομα"
|
||||||
},
|
},
|
||||||
"sectionAdd": {
|
"sectionAdd": {
|
||||||
"message": "Προσθήκη ένος άλλου τμήματος",
|
"message": "Προσθήκη ένος άλλου τμήματος"
|
||||||
"description": "Label for the button to add a section"
|
|
||||||
},
|
},
|
||||||
"sectionCode": {
|
"sectionCode": {
|
||||||
"message": "Κώδικας",
|
"message": "Κώδικας"
|
||||||
"description": "Label for the code for a section"
|
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"sectionRemove": {
|
||||||
"message": "Αφαίρεση ενότητας",
|
"message": "Αφαίρεση ενότητας"
|
||||||
"description": "Label for the button to remove a section"
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"message": "Συντομεύσεις"
|
||||||
|
},
|
||||||
|
"sortDateNewestFirst": {
|
||||||
|
"message": "πιο πρόσφατα πρώτα"
|
||||||
|
},
|
||||||
|
"sortDateOldestFirst": {
|
||||||
|
"message": "πιο παλιά πρώτα"
|
||||||
},
|
},
|
||||||
"styleBadRegexp": {
|
"styleBadRegexp": {
|
||||||
"message": "Το Regexp δεν είναι έγκυρο.",
|
"message": "Το Regexp δεν είναι έγκυρο."
|
||||||
"description": "Validation message for a bad regexp in a style"
|
},
|
||||||
|
"styleBeautify": {
|
||||||
|
"message": "Ωραιοποίηση"
|
||||||
|
},
|
||||||
|
"styleBeautifyIndentConditional": {
|
||||||
|
"message": "Διόρθωση εσοχής για @media και @supports"
|
||||||
|
},
|
||||||
|
"styleBeautifyPreserveNewlines": {
|
||||||
|
"message": "Διατήρηση νέων γραμμών (newlines)"
|
||||||
},
|
},
|
||||||
"styleCancelEditLabel": {
|
"styleCancelEditLabel": {
|
||||||
"message": "Πίσω στη διαχείριση",
|
"message": "Πίσω στη διαχείριση"
|
||||||
"description": "Label for cancel button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleChangesNotSaved": {
|
"styleChangesNotSaved": {
|
||||||
"message": "Έχετε κάνει αλλαγές σε αυτό το ύφος χωρίς αποθήκευση.",
|
"message": "Έχετε κάνει αλλαγές σε αυτό το ύφος χωρίς αποθήκευση."
|
||||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
|
||||||
},
|
},
|
||||||
"styleEnabledLabel": {
|
"styleEnabledLabel": {
|
||||||
"message": "Ενεργοποιημένη",
|
"message": "Ενεργοποιημένη"
|
||||||
"description": "Label for the enabled state of styles"
|
|
||||||
},
|
},
|
||||||
"styleInstall": {
|
"styleInstall": {
|
||||||
"message": "Εγκατάσταση του '$stylename$' στο Stylus;",
|
"message": "Εγκατάσταση του '$stylename$' στο Stylus;",
|
||||||
"description": "Confirmation when installing a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -227,20 +649,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Εισάγετε ένα όνομα",
|
"message": "Εισάγετε ένα όνομα"
|
||||||
"description": "Error displayed when user saves without providing a name"
|
},
|
||||||
|
"styleRegexpTestNone": {
|
||||||
|
"message": "Δε βρέθηκαν καρτέλες που αντιστοιχούν."
|
||||||
},
|
},
|
||||||
"styleSaveLabel": {
|
"styleSaveLabel": {
|
||||||
"message": "Αποθήκευση",
|
"message": "Αποθήκευση"
|
||||||
"description": "Label for save button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"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": {
|
"styleUpdate": {
|
||||||
"message": "Είστε σίγουροι ότι θέλετε να ενημερώσετε το '$stylename$';",
|
"message": "Είστε σίγουροι ότι θέλετε να ενημερώσετε το '$stylename$';",
|
||||||
"description": "Confirmation when updating a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -248,16 +669,43 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stylusUnavailableForURL": {
|
"stylusUnavailableForURL": {
|
||||||
"message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.",
|
"message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή."
|
||||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
},
|
||||||
|
"stylusUnavailableForURLdetails": {
|
||||||
|
"message": "Ως μέτρο ασφαλείας, ο φυλλομετρητής απαγορεύει στα πρόσθετα να επέμβουν στις built-in σελίδες του (όπως π.χ. chrome://version, η σελίδα νέας καρτέλας από το Chrome 61 και μετά, about:addons, κλπ.), καθώς και τις σελίδες άλλωων προσθέτων. Επιπλέον, κάθε φυλλομετρητής περιορίζει την πρόσβαση στο κατάστημα προσθέτων (όπως το Chrome Web Store ή το AMO)."
|
||||||
|
},
|
||||||
|
"syncDropboxStyles": {
|
||||||
|
"message": "Εξαγωγή από το Dropbox"
|
||||||
|
},
|
||||||
|
"syncError": {
|
||||||
|
"message": "Ο συγχρονισμός απέτυχε"
|
||||||
|
},
|
||||||
|
"syncErrorRelogin": {
|
||||||
|
"message": "Ο συγχρονισμός απέτυχε.\nΠροσπαθήστε να συνδεθείτε ξανά στις επιλογές Stylus:\nκάντε κλικ στο 'αποσύνδεση' πρώτα και μετά στο 'σύνδεση'."
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"message": "Αλλαγή στυλ"
|
||||||
|
},
|
||||||
|
"undo": {
|
||||||
|
"message": "Αναίρεση"
|
||||||
|
},
|
||||||
|
"undoGlobal": {
|
||||||
|
"message": "Αναίρεση όλων των ενεργειών"
|
||||||
|
},
|
||||||
|
"unreachableFileHint": {
|
||||||
|
"message": "Το Stylus έχει πρόσβαση στις file:// διευθύνσεις URL μόνο αν έχετε επιλέξει το αντίστοιχο πλαίσιο ελέγχου για το πρόσθετο Stylus στη σελίδα chrome://extensions."
|
||||||
|
},
|
||||||
|
"unzipStyles": {
|
||||||
|
"message": "Αποσυμπίεση στυλ..."
|
||||||
},
|
},
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
"updateAllCheckSucceededNoUpdate": {
|
||||||
"message": "Όλα τα στυλ είναι ενημερωμένα.",
|
"message": "Όλα τα στυλ είναι ενημερωμένα."
|
||||||
"description": "Text that displays when an update all check completed and no updates are available"
|
},
|
||||||
|
"updateAllCheckSucceededSomeEdited": {
|
||||||
|
"message": "Δεν έχει γίνει έλεγχος ενημερώσεων για κάποια στυλ, για να αποφευχθεί η πιθανότητα απώλειας τοπικών επεξεργασιών. Οι ενημερώσεις μπορούν να εξαναγκαστούν ελέγχοντας το κάθε στυλ ξεχωριστά ή ελέγχοντας πάλι όλα τα στυλ (τοπικές επεξεργασίες θα αντικατασταθούν)"
|
||||||
},
|
},
|
||||||
"updateCheckFailBadResponseCode": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "Αποτυχία ενημέρωσης: ο διακομιστής ανταποκρίθηκε με κωδικό $code$.",
|
"message": "Αποτυχία ενημέρωσης: ο διακομιστής ανταποκρίθηκε με κωδικό $code$.",
|
||||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"code": {
|
"code": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -265,23 +713,39 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updateCheckFailServerUnreachable": {
|
"updateCheckFailServerUnreachable": {
|
||||||
"message": "Αποτυχία ενημέρωσης: απρόσιτος διακομιστής.",
|
"message": "Αποτυχία ενημέρωσης: απρόσιτος διακομιστής."
|
||||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
},
|
||||||
|
"updateCheckSkippedLocallyEdited": {
|
||||||
|
"message": "Το στυλ επεξεργάστηκε τοπικά στον υπολογιστή σας."
|
||||||
|
},
|
||||||
|
"updateCheckSkippedMaybeLocallyEdited": {
|
||||||
|
"message": "Το στυλ αυτό μπορεί να έχει επεξεργαστεί τοπικά στον υπολογιστή σας."
|
||||||
},
|
},
|
||||||
"updateCheckSucceededNoUpdate": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "Το στυλ είναι ενημερωμένο.",
|
"message": "Το στυλ είναι ενημερωμένο."
|
||||||
"description": "Text that displays when an update check completed and no update is available"
|
|
||||||
},
|
},
|
||||||
"updateCompleted": {
|
"updateCompleted": {
|
||||||
"message": "Η ενημέρωση ολοκληρώθηκε.",
|
"message": "Η ενημέρωση ολοκληρώθηκε."
|
||||||
"description": "Text that displays when an update completed"
|
},
|
||||||
|
"updatesCurrentlyInstalled": {
|
||||||
|
"message": "Ενημερώσεις που εγκαταστάθηκαν"
|
||||||
|
},
|
||||||
|
"uploadingFile": {
|
||||||
|
"message": "Μεταφόρτωση αρχείου..."
|
||||||
|
},
|
||||||
|
"usercssEditorNamePlaceholder": {
|
||||||
|
"message": "Καθορίστε το @name στον κώδικα"
|
||||||
|
},
|
||||||
|
"versionInvalidOlder": {
|
||||||
|
"message": "Η έκδοση αυτή είναι παλαιότερη από αυτήν που είναι ήδη εγκατεστημένη."
|
||||||
},
|
},
|
||||||
"writeStyleFor": {
|
"writeStyleFor": {
|
||||||
"message": "Γράψτε νέο στυλ για:",
|
"message": "Γράψτε νέο στυλ για:"
|
||||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
|
||||||
},
|
},
|
||||||
"writeStyleForURL": {
|
"writeStyleForURL": {
|
||||||
"message": "αυτή την διεύθυνση URL",
|
"message": "αυτή την διεύθυνση URL"
|
||||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
},
|
||||||
|
"zipStyles": {
|
||||||
|
"message": "Συμπίεση στυλ..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
|
"InaccessibleFileHint": {
|
||||||
|
"message": "Stylus can not access some file types (e.g. pdf & json files).",
|
||||||
|
"description": "Note in the toolbar popup for some file types that cannot be accessed"
|
||||||
|
},
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Write new style",
|
"message": "Write new style",
|
||||||
"description": "Label for the button to go to the add style page"
|
"description": "Label for the button to go to the add style page"
|
||||||
|
|
@ -182,6 +186,9 @@
|
||||||
"message": "Theme",
|
"message": "Theme",
|
||||||
"description": "Label for the style editor's CSS theme."
|
"description": "Label for the style editor's CSS theme."
|
||||||
},
|
},
|
||||||
|
"colorpickerPaletteHint": {
|
||||||
|
"message": "Right-click a swatch to cycle through its source lines"
|
||||||
|
},
|
||||||
"colorpickerSwitchFormatTooltip": {
|
"colorpickerSwitchFormatTooltip": {
|
||||||
"message": "Switch formats: HEX -> RGB -> HSL.\nShift-click to reverse the direction.\nAlso via PgUp (PageUp), PgDn (PageDown) keys.",
|
"message": "Switch formats: HEX -> RGB -> HSL.\nShift-click to reverse the direction.\nAlso via PgUp (PageUp), PgDn (PageDown) keys.",
|
||||||
"description": "Tooltip for the switch button in the color picker popup in the style editor."
|
"description": "Tooltip for the switch button in the color picker popup in the style editor."
|
||||||
|
|
@ -242,6 +249,12 @@
|
||||||
"message": "Yes",
|
"message": "Yes",
|
||||||
"description": "'Yes' button in a confirm dialog"
|
"description": "'Yes' button in a confirm dialog"
|
||||||
},
|
},
|
||||||
|
"connectingDropbox": {
|
||||||
|
"message": "Connecting Dropbox..."
|
||||||
|
},
|
||||||
|
"connectingDropboxNotAllowed": {
|
||||||
|
"message": "Connecting to Dropbox is only available in apps installed directly from the webstore"
|
||||||
|
},
|
||||||
"copied": {
|
"copied": {
|
||||||
"message": "Copied to clipboard",
|
"message": "Copied to clipboard",
|
||||||
"description": "Message shown when content has been copied to the clipboard"
|
"description": "Message shown when content has been copied to the clipboard"
|
||||||
|
|
@ -257,6 +270,42 @@
|
||||||
"message": "Stop using customized name, switch to the style's own name",
|
"message": "Stop using customized name, switch to the style's own name",
|
||||||
"description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles"
|
"description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles"
|
||||||
},
|
},
|
||||||
|
"dateAbbrDay": {
|
||||||
|
"message": "$value$d",
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Day suffix in a short relative date, for example: 8d"
|
||||||
|
},
|
||||||
|
"dateAbbrHour": {
|
||||||
|
"message": "$value$h",
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Hour suffix in a short relative date, for example: 8h"
|
||||||
|
},
|
||||||
|
"dateAbbrMonth": {
|
||||||
|
"message": "$value$m",
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Month suffix in a short relative date, for example: 8m"
|
||||||
|
},
|
||||||
|
"dateAbbrYear": {
|
||||||
|
"message": "$value$y",
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Year suffix in a short relative date, for example: 8y"
|
||||||
|
},
|
||||||
"dateInstalled": {
|
"dateInstalled": {
|
||||||
"message": "Date installed",
|
"message": "Date installed",
|
||||||
"description": "Option text for the user to sort the style by install date"
|
"description": "Option text for the user to sort the style by install date"
|
||||||
|
|
@ -340,6 +389,9 @@
|
||||||
"message": "Export",
|
"message": "Export",
|
||||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||||
},
|
},
|
||||||
|
"exportSavedSuccess": {
|
||||||
|
"message": "File saved with success"
|
||||||
|
},
|
||||||
"externalFeedback": {
|
"externalFeedback": {
|
||||||
"message": "Feedback",
|
"message": "Feedback",
|
||||||
"description": "Label for the external link to send feedback for the style"
|
"description": "Label for the external link to send feedback for the style"
|
||||||
|
|
@ -400,6 +452,9 @@
|
||||||
"message": "Clone",
|
"message": "Clone",
|
||||||
"description": "Used in various places for an action that clones something"
|
"description": "Used in various places for an action that clones something"
|
||||||
},
|
},
|
||||||
|
"genericDescription": {
|
||||||
|
"message": "Description"
|
||||||
|
},
|
||||||
"genericDisabledLabel": {
|
"genericDisabledLabel": {
|
||||||
"message": "Disabled",
|
"message": "Disabled",
|
||||||
"description": "Used in various lists/options to indicate that something is disabled"
|
"description": "Used in various lists/options to indicate that something is disabled"
|
||||||
|
|
@ -440,6 +495,9 @@
|
||||||
"message": "Unknown",
|
"message": "Unknown",
|
||||||
"description": "Used in various parts of the UI to indicate if something is unknown (e.g. an unknown date)"
|
"description": "Used in various parts of the UI to indicate if something is unknown (e.g. an unknown date)"
|
||||||
},
|
},
|
||||||
|
"gettingStyles": {
|
||||||
|
"message": "Getting all styles..."
|
||||||
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "Help",
|
"message": "Help",
|
||||||
"description": "Alternate text for help buttons"
|
"description": "Alternate text for help buttons"
|
||||||
|
|
@ -468,6 +526,12 @@
|
||||||
"message": "Import",
|
"message": "Import",
|
||||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||||
},
|
},
|
||||||
|
"importPreprocessor": {
|
||||||
|
"message": "Style with a <code>@preprocessor</code> won't work in the classic mode. You can switch the editor to Usercss mode: 1) open the style manager, 2) enable \"as Usercss\" checkbox, 3) click \"Write new style\"\n\nImport now anyway?"
|
||||||
|
},
|
||||||
|
"importPreprocessorTitle": {
|
||||||
|
"message": "Potential problem due to @preprocessor"
|
||||||
|
},
|
||||||
"importReplaceLabel": {
|
"importReplaceLabel": {
|
||||||
"message": "Overwrite style",
|
"message": "Overwrite style",
|
||||||
"description": "Label for the button to import and overwrite current style"
|
"description": "Label for the button to import and overwrite current style"
|
||||||
|
|
@ -521,7 +585,7 @@
|
||||||
"description": "Label for install button"
|
"description": "Label for install button"
|
||||||
},
|
},
|
||||||
"installButtonInstalled": {
|
"installButtonInstalled": {
|
||||||
"message": "Style installed",
|
"message": "Style is installed",
|
||||||
"description": "Text displayed when the style is successfully installed"
|
"description": "Text displayed when the style is successfully installed"
|
||||||
},
|
},
|
||||||
"installButtonReinstall": {
|
"installButtonReinstall": {
|
||||||
|
|
@ -573,6 +637,18 @@
|
||||||
"message": "Get styles",
|
"message": "Get styles",
|
||||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
||||||
},
|
},
|
||||||
|
"linkGetStylesInfo": {
|
||||||
|
"message": "This archive site was created by a userstyle community member to back up the slow and unresponsive userstyles.org. The archive updates its contents approximately once a day.",
|
||||||
|
"description": "Info shown when clicking the (i) icon of the uso-archive link in the manager"
|
||||||
|
},
|
||||||
|
"linkGetShareStyles": {
|
||||||
|
"message": "Get and share styles",
|
||||||
|
"description": "Link text for https://userstyles.world/ on the manage page"
|
||||||
|
},
|
||||||
|
"linkGetShareStylesInfo": {
|
||||||
|
"message": "The new community-driven userstyles.world site is created by userstyle authors in order to replace userstyles.org, which has been so slow and unresponsive for the past year that many authors stopped updating their styles.",
|
||||||
|
"description": "Info shown when clicking the (i) icon of the userstyles.world link in the manager"
|
||||||
|
},
|
||||||
"linkStylusWiki": {
|
"linkStylusWiki": {
|
||||||
"message": "Wiki",
|
"message": "Wiki",
|
||||||
"description": "Wiki link text on the manage page e.g. https://github.com/openstyles/stylus/wiki"
|
"description": "Wiki link text on the manage page e.g. https://github.com/openstyles/stylus/wiki"
|
||||||
|
|
@ -730,105 +806,105 @@
|
||||||
},
|
},
|
||||||
"meta_invalidColor": {
|
"meta_invalidColor": {
|
||||||
"message": "Invalid @var color: $color$ is not a color",
|
"message": "Invalid @var color: $color$ is not a color",
|
||||||
"description": "Error displayed when the value of @var color is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"color": {
|
"color": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when the value of @var color is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidNumber": {
|
||||||
|
"message": "Expect a number",
|
||||||
|
"description": "Error displayed when the value is expected to be a number"
|
||||||
},
|
},
|
||||||
"meta_invalidRange": {
|
"meta_invalidRange": {
|
||||||
"message": "Invalid @var $type$: value must be a number or an array",
|
"message": "Invalid @var $type$: value must be a number or an array",
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"type": {
|
"type": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
"meta_invalidRangeMultipleUnits": {
|
|
||||||
"message": "Invalid @var $type$: multiple units are defined",
|
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
|
||||||
"type": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta_invalidRangeTooManyValues": {
|
|
||||||
"message": "Invalid @var $type$: the array contains too many items",
|
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
|
||||||
"type": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta_invalidRangeValue": {
|
|
||||||
"message": "Invalid @var $type$: items in the array must be number, string, or null",
|
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
|
||||||
"type": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"meta_invalidRangeDefault": {
|
"meta_invalidRangeDefault": {
|
||||||
"message": "Invalid @var $type$: default value is null",
|
"message": "Invalid @var $type$: default value is null",
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"type": {
|
"type": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
"meta_invalidRangeMin": {
|
|
||||||
"message": "Invalid @var $type$: default value is lower than the minimum",
|
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
|
||||||
"type": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"meta_invalidRangeMax": {
|
"meta_invalidRangeMax": {
|
||||||
"message": "Invalid @var $type$: default value is larger than the maximum",
|
"message": "Invalid @var $type$: default value is larger than the maximum",
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"type": {
|
"type": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidRangeMin": {
|
||||||
|
"message": "Invalid @var $type$: default value is lower than the minimum",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidRangeMultipleUnits": {
|
||||||
|
"message": "Invalid @var $type$: multiple units are defined",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
},
|
},
|
||||||
"meta_invalidRangeStep": {
|
"meta_invalidRangeStep": {
|
||||||
"message": "Invalid @var $type$: default value is not a mutiple of the step",
|
"message": "Invalid @var $type$: default value is not a mutiple of the step",
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"type": {
|
"type": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidRangeTooManyValues": {
|
||||||
|
"message": "Invalid @var $type$: the array contains too many items",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
},
|
},
|
||||||
"meta_invalidRangeUnits": {
|
"meta_invalidRangeUnits": {
|
||||||
"message": "Invalid @var $type$: '$units$' is not a valid unit",
|
"message": "Invalid @var $type$: '$units$' is not a valid unit",
|
||||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
"placeholders": {
|
||||||
|
"units": {
|
||||||
|
"content": "$2"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidRangeValue": {
|
||||||
|
"message": "Invalid @var $type$: items in the array must be number, string, or null",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"type": {
|
"type": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
},
|
|
||||||
"units": {
|
|
||||||
"content": "$2"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid"
|
||||||
},
|
},
|
||||||
"meta_invalidSelect": {
|
"meta_invalidSelect": {
|
||||||
"message": "Invalid @var select: the default value must be an array or an object",
|
"message": "Invalid @var select: the default value must be an array or an object",
|
||||||
"description": "Error displayed when the value of @var select is invalid"
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
},
|
},
|
||||||
"meta_invalidSelectValue": {
|
|
||||||
"message": "Invalid @var select: values inside the array/object must be a string",
|
|
||||||
"description": "Error displayed when the value of @var select is invalid"
|
|
||||||
},
|
|
||||||
"meta_invalidSelectEmptyOptions": {
|
"meta_invalidSelectEmptyOptions": {
|
||||||
"message": "Invalid @var select: options list is empty",
|
"message": "Invalid @var select: options list is empty",
|
||||||
"description": "Error displayed when the value of @var select is invalid"
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
|
|
@ -845,35 +921,30 @@
|
||||||
"message": "Invalid @var select: option name is duplicated",
|
"message": "Invalid @var select: option name is duplicated",
|
||||||
"description": "Error displayed when the value of @var select is invalid"
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
},
|
},
|
||||||
|
"meta_invalidSelectValue": {
|
||||||
|
"message": "Invalid @var select: values inside the array/object must be a string",
|
||||||
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
|
},
|
||||||
"meta_invalidSelectValueMismatch": {
|
"meta_invalidSelectValueMismatch": {
|
||||||
"message": "Invalid @var select: value doesn't exist in the option list",
|
"message": "Invalid @var select: value doesn't exist in the option list",
|
||||||
"description": "Error displayed when the value of @var select is invalid"
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
},
|
},
|
||||||
|
"meta_invalidString": {
|
||||||
|
"message": "Expect a quoted string",
|
||||||
|
"description": "Error displayed when the value is expected to be a quoted string"
|
||||||
|
},
|
||||||
"meta_invalidURLProtocol": {
|
"meta_invalidURLProtocol": {
|
||||||
"message": "Invalid URL protocol. Only http and https are allowed: $protocol$",
|
"message": "Invalid URL protocol. Only http and https are allowed: $protocol$",
|
||||||
"description": "Error displayed when the protocol of the URL is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"protocol": {
|
"protocol": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when the protocol of the URL is invalid"
|
||||||
},
|
},
|
||||||
"meta_invalidVersion": {
|
"meta_invalidVersion": {
|
||||||
"message": "Invalid version number. The value doesn't match SemVer pattern: $version$",
|
"message": "Invalid version number",
|
||||||
"description": "Error displayed when @version is invalid",
|
"description": "Error displayed when @version is invalid"
|
||||||
"placeholders": {
|
|
||||||
"version": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta_invalidNumber": {
|
|
||||||
"message": "Expect a number",
|
|
||||||
"description": "Error displayed when the value is expected to be a number"
|
|
||||||
},
|
|
||||||
"meta_invalidString": {
|
|
||||||
"message": "Expect a quoted string",
|
|
||||||
"description": "Error displayed when the value is expected to be a quoted string"
|
|
||||||
},
|
},
|
||||||
"meta_invalidWord": {
|
"meta_invalidWord": {
|
||||||
"message": "Expect a word",
|
"message": "Expect a word",
|
||||||
|
|
@ -881,12 +952,12 @@
|
||||||
},
|
},
|
||||||
"meta_missingChar": {
|
"meta_missingChar": {
|
||||||
"message": "Expect characters: $chars$",
|
"message": "Expect characters: $chars$",
|
||||||
"description": "Error displayed when the value is expected to be some characters",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"chars": {
|
"chars": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when the value is expected to be some characters"
|
||||||
},
|
},
|
||||||
"meta_missingEOT": {
|
"meta_missingEOT": {
|
||||||
"message": "Expect EOT data",
|
"message": "Expect EOT data",
|
||||||
|
|
@ -894,56 +965,75 @@
|
||||||
},
|
},
|
||||||
"meta_missingMandatory": {
|
"meta_missingMandatory": {
|
||||||
"message": "Missing mandatory metadata: $keys$",
|
"message": "Missing mandatory metadata: $keys$",
|
||||||
"description": "Error displayed when mandatory keys are missing",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"keys": {
|
"keys": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when mandatory keys are missing"
|
||||||
},
|
},
|
||||||
"meta_unknownJSONLiteral": {
|
"meta_unknownJSONLiteral": {
|
||||||
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
|
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
|
||||||
"description": "Error displayed when JSON value is invalid",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"literal": {
|
"literal": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when JSON value is invalid"
|
||||||
},
|
},
|
||||||
"meta_unknownMeta": {
|
"meta_unknownMeta": {
|
||||||
"message": "Unknown metadata: $key$",
|
"message": "Unknown metadata: $key$",
|
||||||
"description": "Error displayed when unknown metadata is parsed",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"key": {
|
"key": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when unknown metadata is parsed"
|
||||||
},
|
},
|
||||||
"meta_unknownVarType": {
|
"meta_unknownMetaTypo": {
|
||||||
"message": "Unknown @$varkey$ type: $vartype$",
|
"message": "Maybe @$keyOk$? Unknown metadata: @$keyErr$",
|
||||||
"description": "Error displayed when unknown variable type is parsed",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"varkey": {
|
"keyErr": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
},
|
},
|
||||||
"vartype": {
|
"keyOk": {
|
||||||
"content": "$2"
|
"content": "$2"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Try translating it so that at least the first placeholder is visible in our narrow panel. This is the error displayed when an unknown metadata key was sufficiently similar to a known one to consider it a typo."
|
||||||
},
|
},
|
||||||
"meta_unknownPreprocessor": {
|
"meta_unknownPreprocessor": {
|
||||||
"message": "Unknown @preprocessor: $preprocessor$",
|
"message": "Unknown @preprocessor: $preprocessor$",
|
||||||
"description": "Error displayed when unknown @preprocessor is parsed",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"preprocessor": {
|
"preprocessor": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"description": "Error displayed when unknown @preprocessor is parsed"
|
||||||
|
},
|
||||||
|
"meta_unknownVarType": {
|
||||||
|
"message": "Unknown @$varkey$ type: $vartype$",
|
||||||
|
"placeholders": {
|
||||||
|
"vartype": {
|
||||||
|
"content": "$2"
|
||||||
|
},
|
||||||
|
"varkey": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error displayed when unknown variable type is parsed"
|
||||||
|
},
|
||||||
|
"noFileToImport": {
|
||||||
|
"message": "To import your styles, you should export it first."
|
||||||
},
|
},
|
||||||
"noStylesForSite": {
|
"noStylesForSite": {
|
||||||
"message": "No styles installed for this site.",
|
"message": "No styles installed for this site.",
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
},
|
},
|
||||||
|
"numberedLine": {
|
||||||
|
"message": "Line:",
|
||||||
|
"description": "Will be followed by one or more line numbers in the editor."
|
||||||
|
},
|
||||||
"openManage": {
|
"openManage": {
|
||||||
"message": "Manage",
|
"message": "Manage",
|
||||||
"description": "Link to open the manage page."
|
"description": "Link to open the manage page."
|
||||||
|
|
@ -987,6 +1077,12 @@
|
||||||
"optionsAdvancedAutoSwitchSchemeByTime": {
|
"optionsAdvancedAutoSwitchSchemeByTime": {
|
||||||
"message": "By night time:"
|
"message": "By night time:"
|
||||||
},
|
},
|
||||||
|
"optionsAdvancedPatchCsp": {
|
||||||
|
"message": "Patch <code>CSP</code> to allow style assets"
|
||||||
|
},
|
||||||
|
"optionsAdvancedPatchCspNote": {
|
||||||
|
"message": "Enable this if styles contain images or fonts which fail to load on sites with a strict <code>CSP</code> (<code>Content-Security-Policy</code>).\n\nEnabling this setting will relax <code>CSP</code> restrictions, allowing essential style content to load. This option is only intended for advanced users who understand the potential security implications, and accept responsibility for monitoring the content which they're allowing. Read about CSS-based attacks for more information.\n\nAlso be aware, this particular setting is not guaranteed to take effect if another installed extension modifies the network response first."
|
||||||
|
},
|
||||||
"optionsAdvancedStyleViaXhr": {
|
"optionsAdvancedStyleViaXhr": {
|
||||||
"message": "Instant inject mode"
|
"message": "Instant inject mode"
|
||||||
},
|
},
|
||||||
|
|
@ -1014,12 +1110,12 @@
|
||||||
"optionsCustomizePopup": {
|
"optionsCustomizePopup": {
|
||||||
"message": "Popup"
|
"message": "Popup"
|
||||||
},
|
},
|
||||||
"optionsCustomizeUpdate": {
|
|
||||||
"message": "Updates"
|
|
||||||
},
|
|
||||||
"optionsCustomizeSync": {
|
"optionsCustomizeSync": {
|
||||||
"message": "Sync to cloud"
|
"message": "Sync to cloud"
|
||||||
},
|
},
|
||||||
|
"optionsCustomizeUpdate": {
|
||||||
|
"message": "Updates"
|
||||||
|
},
|
||||||
"optionsHeading": {
|
"optionsHeading": {
|
||||||
"message": "Options",
|
"message": "Options",
|
||||||
"description": "Heading for options section on manage page."
|
"description": "Heading for options section on manage page."
|
||||||
|
|
@ -1052,27 +1148,30 @@
|
||||||
"message": "More Options",
|
"message": "More Options",
|
||||||
"description": "Subheading for options section on manage page."
|
"description": "Subheading for options section on manage page."
|
||||||
},
|
},
|
||||||
"optionsUpdateImportNote": {
|
|
||||||
"message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
|
|
||||||
},
|
|
||||||
"optionsUpdateInterval": {
|
|
||||||
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
|
|
||||||
},
|
|
||||||
"optionsSyncNone": {
|
|
||||||
"message": "None"
|
|
||||||
},
|
|
||||||
"optionsSyncConnect": {
|
"optionsSyncConnect": {
|
||||||
"message": "Connect"
|
"message": "Connect"
|
||||||
},
|
},
|
||||||
"optionsSyncDisconnect": {
|
"optionsSyncDisconnect": {
|
||||||
"message": "Disconnect"
|
"message": "Disconnect"
|
||||||
},
|
},
|
||||||
"optionsSyncSyncNow": {
|
|
||||||
"message": "Sync now"
|
|
||||||
},
|
|
||||||
"optionsSyncLogin": {
|
"optionsSyncLogin": {
|
||||||
"message": "Login"
|
"message": "Login"
|
||||||
},
|
},
|
||||||
|
"optionsSyncNone": {
|
||||||
|
"message": "None"
|
||||||
|
},
|
||||||
|
"optionsSyncStatusConnected": {
|
||||||
|
"message": "Connected"
|
||||||
|
},
|
||||||
|
"optionsSyncStatusConnecting": {
|
||||||
|
"message": "Connecting..."
|
||||||
|
},
|
||||||
|
"optionsSyncStatusDisconnected": {
|
||||||
|
"message": "Disconnected"
|
||||||
|
},
|
||||||
|
"optionsSyncStatusDisconnecting": {
|
||||||
|
"message": "Disconnecting..."
|
||||||
|
},
|
||||||
"optionsSyncStatusPull": {
|
"optionsSyncStatusPull": {
|
||||||
"message": "Pulling style $loaded$ of $total$",
|
"message": "Pulling style $loaded$ of $total$",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -1095,20 +1194,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"optionsSyncStatusRelogin": {
|
||||||
|
"message": "Session expired, please login again."
|
||||||
|
},
|
||||||
"optionsSyncStatusSyncing": {
|
"optionsSyncStatusSyncing": {
|
||||||
"message": "Syncing..."
|
"message": "Syncing..."
|
||||||
},
|
},
|
||||||
"optionsSyncStatusConnecting": {
|
"optionsSyncSyncNow": {
|
||||||
"message": "Connecting..."
|
"message": "Sync now"
|
||||||
},
|
},
|
||||||
"optionsSyncStatusConnected": {
|
"optionsUpdateImportNote": {
|
||||||
"message": "Connected"
|
"message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
|
||||||
},
|
},
|
||||||
"optionsSyncStatusDisconnecting": {
|
"optionsUpdateInterval": {
|
||||||
"message": "Disconnecting..."
|
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
|
||||||
},
|
},
|
||||||
"optionsSyncStatusDisconnected": {
|
"overwriteFileExport": {
|
||||||
"message": "Disconnected"
|
"message": "Do you want to overwrite an existing file?"
|
||||||
},
|
},
|
||||||
"paginationCurrent": {
|
"paginationCurrent": {
|
||||||
"message": "Current page",
|
"message": "Current page",
|
||||||
|
|
@ -1187,6 +1289,31 @@
|
||||||
"message": "Temporarily applies the changes without saving.\nSave the style to make the changes permanent.",
|
"message": "Temporarily applies the changes without saving.\nSave the style to make the changes permanent.",
|
||||||
"description": "Tooltip for the checkbox in style editor to enable live preview while editing."
|
"description": "Tooltip for the checkbox in style editor to enable live preview while editing."
|
||||||
},
|
},
|
||||||
|
"publish": {
|
||||||
|
"message": "Publish",
|
||||||
|
"description": "Header for the section to link the style with userStyles.world"
|
||||||
|
},
|
||||||
|
"publishPush": {
|
||||||
|
"message": "Push update",
|
||||||
|
"description": "The 'Publish style' button's new name when a connection is established"
|
||||||
|
},
|
||||||
|
"publishReconnect": {
|
||||||
|
"message": "Try disconnecting then publish again"
|
||||||
|
},
|
||||||
|
"publishRetry": {
|
||||||
|
"message": "Stylus is still trying to publish this style, but you can retry if you see no authentication activity or popups. Retry now?"
|
||||||
|
},
|
||||||
|
"publishStyle": {
|
||||||
|
"message": "Publish style",
|
||||||
|
"description": "Publish the current style to userstyles.world"
|
||||||
|
},
|
||||||
|
"publishUsw": {
|
||||||
|
"message": "Using <userstyles.world>",
|
||||||
|
"description": "Name of the link to https://userstyles.world in the editor"
|
||||||
|
},
|
||||||
|
"readingStyles": {
|
||||||
|
"message": "Reading styles..."
|
||||||
|
},
|
||||||
"reload": {
|
"reload": {
|
||||||
"message": "Reload Stylus extension",
|
"message": "Reload Stylus extension",
|
||||||
"description": "Context menu reload"
|
"description": "Context menu reload"
|
||||||
|
|
@ -1206,6 +1333,9 @@
|
||||||
"retrieveBckp": {
|
"retrieveBckp": {
|
||||||
"message": "Import styles"
|
"message": "Import styles"
|
||||||
},
|
},
|
||||||
|
"retrieveDropboxSync": {
|
||||||
|
"message": "Dropbox Import"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"message": "Search",
|
"message": "Search",
|
||||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||||
|
|
@ -1226,10 +1356,6 @@
|
||||||
"message": "Number of matches in code and applies-to values",
|
"message": "Number of matches in code and applies-to values",
|
||||||
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
|
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
|
||||||
},
|
},
|
||||||
"searchStyleQueryHint": {
|
|
||||||
"message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
|
|
||||||
"description": "Tooltip shown for the text input in the popup's inline style finder"
|
|
||||||
},
|
|
||||||
"searchRegexp": {
|
"searchRegexp": {
|
||||||
"message": "Use /re/ syntax for regexp search",
|
"message": "Use /re/ syntax for regexp search",
|
||||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||||
|
|
@ -1242,6 +1368,12 @@
|
||||||
"message": "No styles found for this site.",
|
"message": "No styles found for this site.",
|
||||||
"description": "Error text in the popup when inline search didn't find any site-specific styles"
|
"description": "Error text in the popup when inline search didn't find any site-specific styles"
|
||||||
},
|
},
|
||||||
|
"searchResultNotMatching": {
|
||||||
|
"message": "The style is installed but it doesn't apply to the current site URL."
|
||||||
|
},
|
||||||
|
"searchResultNotMatchingNote": {
|
||||||
|
"message": "Try asking the author of this userstyle to add the URL.\n\nYou can also open the style in the manager and edit it yourself,\nbut be aware it disables automatic updates for this style."
|
||||||
|
},
|
||||||
"searchResultRating": {
|
"searchResultRating": {
|
||||||
"message": "Rating",
|
"message": "Rating",
|
||||||
"description": "Text for label that shows the search result's rating"
|
"description": "Text for label that shows the search result's rating"
|
||||||
|
|
@ -1254,14 +1386,34 @@
|
||||||
"message": "Weekly installs",
|
"message": "Weekly installs",
|
||||||
"description": "Text for label that shows the number of times a search result was installed during last week"
|
"description": "Text for label that shows the number of times a search result was installed during last week"
|
||||||
},
|
},
|
||||||
"searchStyles": {
|
"searchStyleQueryHint": {
|
||||||
"message": "Search contents",
|
"message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
|
||||||
"description": "Label for the search filter textbox on the Manage styles page"
|
"description": "Tooltip shown for the text input in the popup's inline style finder"
|
||||||
|
},
|
||||||
|
"searchStylesAll": {
|
||||||
|
"message": "All",
|
||||||
|
"description": "Option for `find styles` scope selector in the manager."
|
||||||
|
},
|
||||||
|
"searchStylesCode": {
|
||||||
|
"message": "CSS code",
|
||||||
|
"description": "Option for `find styles` scope selector in the manager."
|
||||||
},
|
},
|
||||||
"searchStylesHelp": {
|
"searchStylesHelp": {
|
||||||
"message": "</> key focuses the search field.\nPlain text: search within the name, code, homepage URL and sites it is applied to. Words with less than 3 letters are ignored.\nStyles matching a full URL: prefix the search with <url:>, e.g. <url:https://github.com/openstyles/stylus>\nRegular expressions: include slashes and flags, e.g. </body.*?\\ba\\b/simguy>\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">",
|
"message": "</> or <Ctrl-F> key focuses the search field.\nDefault mode is plain text search for all space-separated terms in any order.\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">\nRegular expressions: include slashes and flags, e.g. </body.*?\\ba\\b/i>\n\"By URL\" in scope selector: finds styles that apply to a fully specified URL e.g. https://www.example.org/\n\"Metadata\" in scope selector: searches in names, \"applies to\" specifiers, installation URL, update URL, and the entire metadata block for usercss styles.",
|
||||||
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page"
|
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page"
|
||||||
},
|
},
|
||||||
|
"searchStylesMatchUrl": {
|
||||||
|
"message": "By URL",
|
||||||
|
"description": "Option for `find styles` scope selector in the manager. See searchMatchUrlHint for more info."
|
||||||
|
},
|
||||||
|
"searchStylesMeta": {
|
||||||
|
"message": "Metadata",
|
||||||
|
"description": "Option for `find styles` scope selector in the manager."
|
||||||
|
},
|
||||||
|
"searchStylesName": {
|
||||||
|
"message": "Name",
|
||||||
|
"description": "Option for `find styles` scope selector in the manager."
|
||||||
|
},
|
||||||
"sectionAdd": {
|
"sectionAdd": {
|
||||||
"message": "Add another section",
|
"message": "Add another section",
|
||||||
"description": "Label for the button to add a section"
|
"description": "Label for the button to add a section"
|
||||||
|
|
@ -1278,6 +1430,10 @@
|
||||||
"message": "Restore removed section",
|
"message": "Restore removed section",
|
||||||
"description": "Label for the button to restore a removed section"
|
"description": "Label for the button to restore a removed section"
|
||||||
},
|
},
|
||||||
|
"sections": {
|
||||||
|
"message": "Sections",
|
||||||
|
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
|
||||||
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"message": "Shortcuts",
|
"message": "Shortcuts",
|
||||||
"description": "Go to shortcut configuration"
|
"description": "Go to shortcut configuration"
|
||||||
|
|
@ -1394,6 +1550,9 @@
|
||||||
"message": "Mozilla Format",
|
"message": "Mozilla Format",
|
||||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||||
},
|
},
|
||||||
|
"styleName": {
|
||||||
|
"message": "Style name"
|
||||||
|
},
|
||||||
"styleNotAppliedRegexpProblemTooltip": {
|
"styleNotAppliedRegexpProblemTooltip": {
|
||||||
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
|
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
|
||||||
"description": "Tooltip in the popup for styles that were not applied at all"
|
"description": "Tooltip in the popup for styles that were not applied at all"
|
||||||
|
|
@ -1475,6 +1634,20 @@
|
||||||
"message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version, the standard new tab page as of Chrome 61, about:addons, and so on) as well as other extensions' pages. Each browser also restricts access to its own extensions gallery (like Chrome Web Store or AMO).",
|
"message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version, the standard new tab page as of Chrome 61, about:addons, and so on) as well as other extensions' pages. Each browser also restricts access to its own extensions gallery (like Chrome Web Store or AMO).",
|
||||||
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
|
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||||
},
|
},
|
||||||
|
"syncDropboxDeprecated": {
|
||||||
|
"message": "Dropbox import/export is replaced by a more advanced style sync in the options page."
|
||||||
|
},
|
||||||
|
"syncDropboxStyles": {
|
||||||
|
"message": "Dropbox Export"
|
||||||
|
},
|
||||||
|
"syncError": {
|
||||||
|
"message": "Sync failed",
|
||||||
|
"description": "Tooltip for the toolbar icon"
|
||||||
|
},
|
||||||
|
"syncErrorRelogin": {
|
||||||
|
"message": "Sync failed.\nTry to re-login in Stylus options:\nclick 'disconnect' first, then 'connect'.",
|
||||||
|
"description": "Tooltip for the toolbar icon"
|
||||||
|
},
|
||||||
"syncStorageErrorSaving": {
|
"syncStorageErrorSaving": {
|
||||||
"message": "The value cannot be saved. Try reducing the amount of text.",
|
"message": "The value cannot be saved. Try reducing the amount of text.",
|
||||||
"description": "Displayed when trying to save an excessively big value via storage.sync API"
|
"description": "Displayed when trying to save an excessively big value via storage.sync API"
|
||||||
|
|
@ -1499,14 +1672,6 @@
|
||||||
"message": "To allow access open <about:config>, right-click the list, click 'New', then 'Boolean', paste <privacy.resistFingerprinting.block_mozAddonManager> and click OK, <true>, OK, reload the <addons.mozilla.org> page.",
|
"message": "To allow access open <about:config>, right-click the list, click 'New', then 'Boolean', paste <privacy.resistFingerprinting.block_mozAddonManager> and click OK, <true>, OK, reload the <addons.mozilla.org> page.",
|
||||||
"description": "Note in the popup when opened on addons.mozilla.org in Firefox >= 59"
|
"description": "Note in the popup when opened on addons.mozilla.org in Firefox >= 59"
|
||||||
},
|
},
|
||||||
"unreachableAMOHintNewFF": {
|
|
||||||
"message": "In Firefox 60 and newer you'll also have to remove AMO domain from <extensions.webextensions.restrictedDomains> in <about:config>.",
|
|
||||||
"description": "Note in the popup when opened on addons.mozilla.org in Firefox >= 59"
|
|
||||||
},
|
|
||||||
"unreachableAMOHintOldFF": {
|
|
||||||
"message": "Only Firefox 59 and newer can be configured to allow WebExtensions to add style elements on CSP-protected sites such as this one.",
|
|
||||||
"description": "Note in the popup when opened on addons.mozilla.org in Firefox < 59"
|
|
||||||
},
|
|
||||||
"unreachableContentScript": {
|
"unreachableContentScript": {
|
||||||
"message": "Could not communicate with the page. Try reloading the tab.",
|
"message": "Could not communicate with the page. Try reloading the tab.",
|
||||||
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
|
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
|
||||||
|
|
@ -1515,9 +1680,16 @@
|
||||||
"message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.",
|
"message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.",
|
||||||
"description": "Note in the toolbar popup for file:// URLs"
|
"description": "Note in the toolbar popup for file:// URLs"
|
||||||
},
|
},
|
||||||
"InaccessibleFileHint": {
|
"unreachableMozSiteHint": {
|
||||||
"message": "Stylus can not access some file types (e.g. pdf & json files).",
|
"message": "In Firefox 60 and newer you need to remove this domain from <extensions.webextensions.restrictedDomains> in <about:config>.",
|
||||||
"description": "Note in the toolbar popup for some file types that cannot be accessed"
|
"description": "Note in the popup when opened on a restricted mozilla site in Firefox >= 60"
|
||||||
|
},
|
||||||
|
"unreachableMozSiteHintOldFF": {
|
||||||
|
"message": "Only Firefox 59 and newer can be configured to allow WebExtensions to add style elements on CSP-protected sites such as this one.",
|
||||||
|
"description": "Note in the popup when opened on a restricted mozilla site in Firefox < 59"
|
||||||
|
},
|
||||||
|
"unzipStyles": {
|
||||||
|
"message": "Unzipping styles..."
|
||||||
},
|
},
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
"updateAllCheckSucceededNoUpdate": {
|
||||||
"message": "No updates found.",
|
"message": "No updates found.",
|
||||||
|
|
@ -1571,6 +1743,9 @@
|
||||||
"message": "Updates installed:",
|
"message": "Updates installed:",
|
||||||
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
||||||
},
|
},
|
||||||
|
"uploadingFile": {
|
||||||
|
"message": "Uploading File..."
|
||||||
|
},
|
||||||
"usercssAvoidOverwriting": {
|
"usercssAvoidOverwriting": {
|
||||||
"message": "Please change the value of @name or @namespace to avoid overwriting an existing style.",
|
"message": "Please change the value of @name or @namespace to avoid overwriting an existing style.",
|
||||||
"description": "Shown in a message box when attempting to save a new Usercss style that would overwrite an existing one."
|
"description": "Shown in a message box when attempting to save a new Usercss style that would overwrite an existing one."
|
||||||
|
|
@ -1605,43 +1780,7 @@
|
||||||
"message": "this URL",
|
"message": "this URL",
|
||||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||||
},
|
},
|
||||||
"syncDropboxStyles": {
|
|
||||||
"message": "Dropbox Export"
|
|
||||||
},
|
|
||||||
"syncDropboxDeprecated": {
|
|
||||||
"message": "Dropbox import/export is replaced by a more advanced style sync in the options page."
|
|
||||||
},
|
|
||||||
"retrieveDropboxSync": {
|
|
||||||
"message": "Dropbox Import"
|
|
||||||
},
|
|
||||||
"overwriteFileExport": {
|
|
||||||
"message": "Do you want to overwrite an existing file?"
|
|
||||||
},
|
|
||||||
"exportSavedSuccess": {
|
|
||||||
"message": "File saved with success"
|
|
||||||
},
|
|
||||||
"noFileToImport": {
|
|
||||||
"message": "To import your styles, you should export it first."
|
|
||||||
},
|
|
||||||
"connectingDropbox": {
|
|
||||||
"message": "Connecting Dropbox..."
|
|
||||||
},
|
|
||||||
"connectingDropboxNotAllowed": {
|
|
||||||
"message": "Connecting to Dropbox is only available in apps installed directly from the webstore"
|
|
||||||
},
|
|
||||||
"gettingStyles": {
|
|
||||||
"message": "Getting all styles..."
|
|
||||||
},
|
|
||||||
"zipStyles": {
|
"zipStyles": {
|
||||||
"message": "Zipping styles..."
|
"message": "Zipping styles..."
|
||||||
},
|
|
||||||
"unzipStyles": {
|
|
||||||
"message": "Unzipping styles..."
|
|
||||||
},
|
|
||||||
"readingStyles": {
|
|
||||||
"message": "Reading styles..."
|
|
||||||
},
|
|
||||||
"uploadingFile": {
|
|
||||||
"message": "Uploading File..."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,51 @@
|
||||||
{
|
{
|
||||||
"appliesRemoveError": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"colorpickerTooltip": {
|
||||||
"message": "Open colour picker",
|
"message": "Open colour picker"
|
||||||
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
|
|
||||||
},
|
},
|
||||||
"description": {
|
"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": {
|
"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": {
|
"editStyleHeading": {
|
||||||
"message": "Edit style",
|
"message": "Edit style"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"message": "Licence",
|
"message": "Licence"
|
||||||
"description": "Label for the license"
|
|
||||||
},
|
},
|
||||||
"manageFaviconsGray": {
|
"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": {
|
"optionsBadgeDisabled": {
|
||||||
"message": "Background colour when disabled",
|
"message": "Background colour when disabled"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsBadgeNormal": {
|
"optionsBadgeNormal": {
|
||||||
"message": "Background colour",
|
"message": "Background colour"
|
||||||
"description": ""
|
|
||||||
},
|
},
|
||||||
"optionsUpdateImportNote": {
|
"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": {
|
"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": {
|
"styleInstallFailed": {
|
||||||
"message": "Failed to install userstyle\n$error$",
|
"message": "Failed to install userstyle\n$error$",
|
||||||
"description": "Warning when installation failed",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"error": {
|
"error": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -69,15 +53,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styleRegexpPartialExplanation": {
|
"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": {
|
"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": {
|
"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
|
|
@ -1,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Uusi Tyyli",
|
"message": "Uusi Tyyli"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Lisää Tyyli",
|
"message": "Lisää Tyyli"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Lisää",
|
"message": "Lisää"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Kooskee: $applies$",
|
"message": "Kooskee: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,80 +17,61 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "ja lisää",
|
"message": "ja lisää"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "URL ositteita domainilla",
|
"message": "URL ositteita domainilla"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesHelp": {
|
"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": {
|
"appliesLabel": {
|
||||||
"message": "Koskee",
|
"message": "Koskee"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"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": {
|
"appliesRemove": {
|
||||||
"message": "Poista",
|
"message": "Poista"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Tarkenna",
|
"message": "Tarkenna"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Kaikki",
|
"message": "Kaikki"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"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": {
|
"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": {
|
"checkForUpdate": {
|
||||||
"message": "Hae päivityksiä",
|
"message": "Hae päivityksiä"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Tarkistetaan...",
|
"message": "Tarkistetaan..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"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": {
|
"deleteStyleLabel": {
|
||||||
"message": "Poista",
|
"message": "Poista"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"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": {
|
"disableStyleLabel": {
|
||||||
"message": "Poista Käytöstä",
|
"message": "Poista Käytöstä"
|
||||||
"description": "Label for the button to disable a style"
|
|
||||||
},
|
},
|
||||||
"editStyleHeading": {
|
"editStyleHeading": {
|
||||||
"message": "Muokkaa Tyyliä",
|
"message": "Muokkaa Tyyliä"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "Muokkaa",
|
"message": "Muokkaa"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"editStyleTitle": {
|
"editStyleTitle": {
|
||||||
"message": "Muokkaa Tyyliä $stylename$",
|
"message": "Muokkaa Tyyliä $stylename$",
|
||||||
"description": "Title of the page for editing styles",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -102,76 +79,58 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"enableStyleLabel": {
|
||||||
"message": "Aktivoi",
|
"message": "Aktivoi"
|
||||||
"description": "Label for the button to enable a style"
|
|
||||||
},
|
},
|
||||||
"findStylesForSite": {
|
"findStylesForSite": {
|
||||||
"message": "Hae lisää tyylejä tälle sivustolle",
|
"message": "Hae lisää tyylejä tälle sivustolle"
|
||||||
"description": "Text for a link that gets a list of styles for the current site"
|
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "Apu",
|
"message": "Apu"
|
||||||
"description": "Alternate text for help buttons"
|
|
||||||
},
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "Asenna päivitys",
|
"message": "Asenna päivitys"
|
||||||
"description": "Label for the button to install an update for a single style"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "Asennetut Tyylit",
|
"message": "Asennetut Tyylit"
|
||||||
"description": "Heading for the manage page"
|
|
||||||
},
|
},
|
||||||
"manageTitle": {
|
"manageTitle": {
|
||||||
"message": "Tyylikäs",
|
"message": "Tyylikäs"
|
||||||
"description": "Title for the manage page"
|
|
||||||
},
|
},
|
||||||
"noStylesForSite": {
|
"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": {
|
"openManage": {
|
||||||
"message": "Hallitse asennettuja tyylejä",
|
"message": "Hallitse asennettuja tyylejä"
|
||||||
"description": "Link to open the manage page."
|
|
||||||
},
|
},
|
||||||
"popupStylesFirst": {
|
"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": {
|
"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": {
|
"sectionAdd": {
|
||||||
"message": "Lisää uusi osio",
|
"message": "Lisää uusi osio"
|
||||||
"description": "Label for the button to add a section"
|
|
||||||
},
|
},
|
||||||
"sectionCode": {
|
"sectionCode": {
|
||||||
"message": "Koodi",
|
"message": "Koodi"
|
||||||
"description": "Label for the code for a section"
|
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"sectionRemove": {
|
||||||
"message": "Poista osio",
|
"message": "Poista osio"
|
||||||
"description": "Label for the button to remove a section"
|
|
||||||
},
|
},
|
||||||
"styleBadRegexp": {
|
"styleBadRegexp": {
|
||||||
"message": "Regexp ei kelpaa.",
|
"message": "Regexp ei kelpaa."
|
||||||
"description": "Validation message for a bad regexp in a style"
|
|
||||||
},
|
},
|
||||||
"styleCancelEditLabel": {
|
"styleCancelEditLabel": {
|
||||||
"message": "Takaisin hallintapaneeliin",
|
"message": "Takaisin hallintapaneeliin"
|
||||||
"description": "Label for cancel button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleChangesNotSaved": {
|
"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": {
|
"styleEnabledLabel": {
|
||||||
"message": "Aktivoitu",
|
"message": "Aktivoitu"
|
||||||
"description": "Label for the enabled state of styles"
|
|
||||||
},
|
},
|
||||||
"styleInstall": {
|
"styleInstall": {
|
||||||
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
"message": "Asennetaanko '$stylename$' Stylusiin?",
|
||||||
"description": "Confirmation when installing a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -179,24 +138,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Syötä nimi",
|
"message": "Syötä nimi"
|
||||||
"description": "Error displayed when user saves without providing a name"
|
|
||||||
},
|
},
|
||||||
"styleSaveLabel": {
|
"styleSaveLabel": {
|
||||||
"message": "Tallenna",
|
"message": "Tallenna"
|
||||||
"description": "Label for save button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"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": {
|
"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": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
|
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
|
||||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"code": {
|
"code": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -204,15 +158,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updateCheckFailServerUnreachable": {
|
"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": {
|
"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": {
|
"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,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Nije styl skriuwe",
|
"message": "Nije styl skriuwe"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Styl tafoegje",
|
"message": "Styl tafoegje"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Tafoegje",
|
"message": "Tafoegje"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Fan tapassing op: $applies$",
|
"message": "Fan tapassing op: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,107 +17,81 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "en mear",
|
"message": "en mear"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"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": {
|
"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": {
|
"appliesLabel": {
|
||||||
"message": "Fan tapassing op",
|
"message": "Fan tapassing op"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"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": {
|
"appliesRemove": {
|
||||||
"message": "Fuortsmite",
|
"message": "Fuortsmite"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Spesifisearje",
|
"message": "Spesifisearje"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Alles",
|
"message": "Alles"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"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": {
|
"applyAllUpdates": {
|
||||||
"message": "Alle fernijingen tapasse",
|
"message": "Alle fernijingen tapasse"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"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": {
|
"checkForUpdate": {
|
||||||
"message": "Kontrolearje op fernijing",
|
"message": "Kontrolearje op fernijing"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Kontrolearje...",
|
"message": "Kontrolearje..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"cm_indentWithTabs": {
|
"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": {
|
"cm_keyMap": {
|
||||||
"message": "Toetseboerdyndieling",
|
"message": "Toetseboerdyndieling"
|
||||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_lineWrapping": {
|
"cm_lineWrapping": {
|
||||||
"message": "Teksttebekrin",
|
"message": "Teksttebekrin"
|
||||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_smartIndent": {
|
"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": {
|
"cm_tabSize": {
|
||||||
"message": "Ljepblêdgrutte",
|
"message": "Ljepblêdgrutte"
|
||||||
"description": "Label for the text box controlling tab size option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_theme": {
|
"cm_theme": {
|
||||||
"message": "Tema",
|
"message": "Tema"
|
||||||
"description": "Label for the style editor's CSS theme."
|
|
||||||
},
|
},
|
||||||
"confirmNo": {
|
"confirmNo": {
|
||||||
"message": "Nee",
|
"message": "Nee"
|
||||||
"description": "'No' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmStop": {
|
"confirmStop": {
|
||||||
"message": "Stoppe",
|
"message": "Stoppe"
|
||||||
"description": "'Stop' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmYes": {
|
"confirmYes": {
|
||||||
"message": "Ja",
|
"message": "Ja"
|
||||||
"description": "'Yes' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"dbError": {
|
"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": {
|
"defaultTheme": {
|
||||||
"message": "standert",
|
"message": "standert"
|
||||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"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": {
|
"deleteStyleLabel": {
|
||||||
"message": "Fuortsmite",
|
"message": "Fuortsmite"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Engadir Estilo",
|
"message": "Engadir Estilo"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"alphaChannel": {
|
"alphaChannel": {
|
||||||
"message": "Opacidade",
|
"message": "Opacidade"
|
||||||
"description": "Label of color's opacity"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Engadir",
|
"message": "Engadir"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Aplica a: $applies$",
|
"message": "Aplica a: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,47 +17,36 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "e mais",
|
"message": "e mais"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "URLs no dominio",
|
"message": "URLs no dominio"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesLabel": {
|
"appliesLabel": {
|
||||||
"message": "Aplica para",
|
"message": "Aplica para"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesLineWidgetWarning": {
|
"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": {
|
"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": {
|
"appliesRemove": {
|
||||||
"message": "Suprimir",
|
"message": "Suprimir"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Especificar",
|
"message": "Especificar"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Todo",
|
"message": "Todo"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"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": {
|
"applyAllUpdates": {
|
||||||
"message": "Aplicar tódalas actualizacións",
|
"message": "Aplicar tódalas actualizacións"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"message": "Autor",
|
"message": "Autor"
|
||||||
"description": "Label for the style author"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
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,19 +1,15 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "Упиши нови стил",
|
"message": "Упиши нови стил"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"addStyleTitle": {
|
"addStyleTitle": {
|
||||||
"message": "Додај стил",
|
"message": "Додај стил"
|
||||||
"description": "Title of the page for adding styles"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Додај",
|
"message": "Додај"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "Примењује се на: $applies$",
|
"message": "Примењује се на: $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -21,140 +17,106 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "и још",
|
"message": "и још"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesDomainOption": {
|
"appliesDomainOption": {
|
||||||
"message": "УРЛ адресе на домену",
|
"message": "УРЛ адресе на домену"
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
|
||||||
},
|
},
|
||||||
"appliesHelp": {
|
"appliesHelp": {
|
||||||
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.",
|
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује."
|
||||||
"description": "Help text for 'applies to' section"
|
|
||||||
},
|
},
|
||||||
"appliesLabel": {
|
"appliesLabel": {
|
||||||
"message": "Примењује се на",
|
"message": "Примењује се на"
|
||||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
|
||||||
},
|
},
|
||||||
"appliesRegexpOption": {
|
"appliesRegexpOption": {
|
||||||
"message": "УРЛ адресе које одговарају регуларном изразу",
|
"message": "УРЛ адресе које одговарају регуларном изразу"
|
||||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"appliesRemove": {
|
||||||
"message": "Уклони",
|
"message": "Уклони"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Детаљније",
|
"message": "Детаљније"
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "Све",
|
"message": "Све"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"appliesUrlOption": {
|
"appliesUrlOption": {
|
||||||
"message": "УРЛ",
|
"message": "УРЛ"
|
||||||
"description": "Option to make the style apply to the entered string as a URL"
|
|
||||||
},
|
},
|
||||||
"appliesUrlPrefixOption": {
|
"appliesUrlPrefixOption": {
|
||||||
"message": "УРЛ адресе које почињу са",
|
"message": "УРЛ адресе које почињу са"
|
||||||
"description": "Option to make the style apply to the entered string as a URL prefix"
|
|
||||||
},
|
},
|
||||||
"applyAllUpdates": {
|
"applyAllUpdates": {
|
||||||
"message": "Примени сва ажурирања",
|
"message": "Примени сва ажурирања"
|
||||||
"description": "Label for the button to apply all detected updates"
|
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"checkAllUpdates": {
|
||||||
"message": "Проверите ажурирања за све стилове",
|
"message": "Проверите ажурирања за све стилове"
|
||||||
"description": "Label for the button to check all styles for updates"
|
|
||||||
},
|
},
|
||||||
"checkForUpdate": {
|
"checkForUpdate": {
|
||||||
"message": "Проверите ажурирање",
|
"message": "Проверите ажурирање"
|
||||||
"description": "Label for the button to check a single style for an update"
|
|
||||||
},
|
},
|
||||||
"checkingForUpdate": {
|
"checkingForUpdate": {
|
||||||
"message": "Проверавање...",
|
"message": "Проверавање..."
|
||||||
"description": "Text to display when checking a style for an update"
|
|
||||||
},
|
},
|
||||||
"cm_indentWithTabs": {
|
"cm_indentWithTabs": {
|
||||||
"message": "Користи картице са паметним увлачењем редова",
|
"message": "Користи картице са паметним увлачењем редова"
|
||||||
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_keyMap": {
|
"cm_keyMap": {
|
||||||
"message": "Мапа тастера",
|
"message": "Мапа тастера"
|
||||||
"description": "Label for the drop-down list controlling the keymap for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_lineWrapping": {
|
"cm_lineWrapping": {
|
||||||
"message": "Преламање текста",
|
"message": "Преламање текста"
|
||||||
"description": "Label for the checkbox controlling word wrap option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_smartIndent": {
|
"cm_smartIndent": {
|
||||||
"message": "Користи паметно увлачење редова",
|
"message": "Користи паметно увлачење редова"
|
||||||
"description": "Label for the checkbox controlling smart indentation option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_tabSize": {
|
"cm_tabSize": {
|
||||||
"message": "Величина картице",
|
"message": "Величина картице"
|
||||||
"description": "Label for the text box controlling tab size option for the style editor."
|
|
||||||
},
|
},
|
||||||
"cm_theme": {
|
"cm_theme": {
|
||||||
"message": "Тема",
|
"message": "Тема"
|
||||||
"description": "Label for the style editor's CSS theme."
|
|
||||||
},
|
},
|
||||||
"confirmNo": {
|
"confirmNo": {
|
||||||
"message": "Не",
|
"message": "Не"
|
||||||
"description": "'No' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmStop": {
|
"confirmStop": {
|
||||||
"message": "Заустави",
|
"message": "Заустави"
|
||||||
"description": "'Stop' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"confirmYes": {
|
"confirmYes": {
|
||||||
"message": "Да",
|
"message": "Да"
|
||||||
"description": "'Yes' button in a confirm dialog"
|
|
||||||
},
|
},
|
||||||
"dbError": {
|
"dbError": {
|
||||||
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?",
|
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?"
|
||||||
"description": "Prompt when a DB error is encountered"
|
|
||||||
},
|
},
|
||||||
"defaultTheme": {
|
"defaultTheme": {
|
||||||
"message": "подразумевано",
|
"message": "подразумевано"
|
||||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"deleteStyleConfirm": {
|
||||||
"message": "Да ли сте сигурни да желите да избришете овај стил?",
|
"message": "Да ли сте сигурни да желите да избришете овај стил?"
|
||||||
"description": "Confirmation before deleting a style"
|
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"deleteStyleLabel": {
|
||||||
"message": "Избриши",
|
"message": "Избриши"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.",
|
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове."
|
||||||
"description": "Extension description"
|
|
||||||
},
|
},
|
||||||
"disableAllStyles": {
|
"disableAllStyles": {
|
||||||
"message": "Искључи све стилове",
|
"message": "Искључи све стилове"
|
||||||
"description": "Label for the checkbox that turns all enabled styles off."
|
|
||||||
},
|
},
|
||||||
"disableStyleLabel": {
|
"disableStyleLabel": {
|
||||||
"message": "Онемогући",
|
"message": "Онемогући"
|
||||||
"description": "Label for the button to disable a style"
|
|
||||||
},
|
},
|
||||||
"editGotoLine": {
|
"editGotoLine": {
|
||||||
"message": "Иди на ред (или line:col)",
|
"message": "Иди на ред (или line:col)"
|
||||||
"description": "Go to line or line:column on Ctrl-G in style code editor"
|
|
||||||
},
|
},
|
||||||
"editStyleHeading": {
|
"editStyleHeading": {
|
||||||
"message": "Уреди стил",
|
"message": "Уреди стил"
|
||||||
"description": "Title of the page for editing styles"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "Уреди",
|
"message": "Уреди"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"editStyleTitle": {
|
"editStyleTitle": {
|
||||||
"message": "Уреди стил $stylename$",
|
"message": "Уреди стил $stylename$",
|
||||||
"description": "Title of the page for editing styles",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -162,68 +124,52 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"enableStyleLabel": {
|
||||||
"message": "Омогући",
|
"message": "Омогући"
|
||||||
"description": "Label for the button to enable a style"
|
|
||||||
},
|
},
|
||||||
"exportLabel": {
|
"exportLabel": {
|
||||||
"message": "Извези",
|
"message": "Извези"
|
||||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
|
||||||
},
|
},
|
||||||
"findStylesForSite": {
|
"findStylesForSite": {
|
||||||
"message": "Пронађи још стилова за овај сајт",
|
"message": "Пронађи још стилова за овај сајт"
|
||||||
"description": "Text for a link that gets a list of styles for the current site"
|
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "Помоћ",
|
"message": "Помоћ"
|
||||||
"description": "Alternate text for help buttons"
|
|
||||||
},
|
},
|
||||||
"helpKeyMapCommand": {
|
"helpKeyMapCommand": {
|
||||||
"message": "Укуцај име команде",
|
"message": "Укуцај име команде"
|
||||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
|
||||||
},
|
},
|
||||||
"helpKeyMapHotkey": {
|
"helpKeyMapHotkey": {
|
||||||
"message": "Притисни пречицу",
|
"message": "Притисни пречицу"
|
||||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
|
||||||
},
|
},
|
||||||
"importAppendLabel": {
|
"importAppendLabel": {
|
||||||
"message": "Додај стилу",
|
"message": "Додај стилу"
|
||||||
"description": "Label for the button to import a style and append to the existing sections"
|
|
||||||
},
|
},
|
||||||
"importAppendTooltip": {
|
"importAppendTooltip": {
|
||||||
"message": "Додај увезени стил тренутном стилу",
|
"message": "Додај увезени стил тренутном стилу"
|
||||||
"description": "Tooltip for the button to import a style and append to the existing sections"
|
|
||||||
},
|
},
|
||||||
"importLabel": {
|
"importLabel": {
|
||||||
"message": "Увези",
|
"message": "Увези"
|
||||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
|
||||||
},
|
},
|
||||||
"importReplaceLabel": {
|
"importReplaceLabel": {
|
||||||
"message": "Упиши преко стила",
|
"message": "Упиши преко стила"
|
||||||
"description": "Label for the button to import and overwrite current style"
|
|
||||||
},
|
},
|
||||||
"importReplaceTooltip": {
|
"importReplaceTooltip": {
|
||||||
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил",
|
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил"
|
||||||
"description": "Label for the button to import and overwrite current style"
|
|
||||||
},
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "Инсталирај ажурирање",
|
"message": "Инсталирај ажурирање"
|
||||||
"description": "Label for the button to install an update for a single style"
|
|
||||||
},
|
},
|
||||||
"linkGetHelp": {
|
"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": {
|
"linkGetStyles": {
|
||||||
"message": "Преузмите стилове",
|
"message": "Преузмите стилове"
|
||||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
|
||||||
},
|
},
|
||||||
"linterIssues": {
|
"linterIssues": {
|
||||||
"message": "Проблеми",
|
"message": "Проблеми"
|
||||||
"description": "Label for the CSS linter issues block on the style edit page"
|
|
||||||
},
|
},
|
||||||
"linterIssuesHelp": {
|
"linterIssuesHelp": {
|
||||||
"message": "Проблем пронађен од стране $link$:",
|
"message": "Проблем пронађен од стране $link$:",
|
||||||
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"link": {
|
"link": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -231,104 +177,76 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"manageFilters": {
|
"manageFilters": {
|
||||||
"message": "Филтери",
|
"message": "Филтери"
|
||||||
"description": "Label for filters container"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "Инсталирани стилови",
|
"message": "Инсталирани стилови"
|
||||||
"description": "Heading for the manage page"
|
|
||||||
},
|
},
|
||||||
"manageOnlyEnabled": {
|
"manageOnlyEnabled": {
|
||||||
"message": "Само омогућени стилови",
|
"message": "Само омогућени стилови"
|
||||||
"description": "Checkbox to show only enabled styles"
|
|
||||||
},
|
},
|
||||||
"menuShowBadge": {
|
"menuShowBadge": {
|
||||||
"message": "Прикажи број активних стилова",
|
"message": "Прикажи број активних стилова"
|
||||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
|
||||||
},
|
},
|
||||||
"noStylesForSite": {
|
"noStylesForSite": {
|
||||||
"message": "Нема инсталираних стилова за овај сајт.",
|
"message": "Нема инсталираних стилова за овај сајт."
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
|
||||||
},
|
},
|
||||||
"openManage": {
|
"openManage": {
|
||||||
"message": "Управљај инсталираним стиловима",
|
"message": "Управљај инсталираним стиловима"
|
||||||
"description": "Link to open the manage page."
|
|
||||||
},
|
},
|
||||||
"optionsHeading": {
|
"optionsHeading": {
|
||||||
"message": "Опције",
|
"message": "Опције"
|
||||||
"description": "Heading for options section on manage page."
|
|
||||||
},
|
},
|
||||||
"popupStylesFirst": {
|
"popupStylesFirst": {
|
||||||
"message": "Излистај стилове пре команди у менију дугмета на алатној траци",
|
"message": "Излистај стилове пре команди у менију дугмета на алатној траци"
|
||||||
"description": "Label for the checkbox controlling section order in the popup."
|
|
||||||
},
|
},
|
||||||
"prefShowBadge": {
|
"prefShowBadge": {
|
||||||
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци",
|
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци"
|
||||||
"description": "Label for the checkbox controlling toolbar badge text."
|
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
"message": "Замени",
|
"message": "Замени"
|
||||||
"description": "Label before the replace input field in the editor shown on Ctrl-H"
|
|
||||||
},
|
},
|
||||||
"replaceAll": {
|
"replaceAll": {
|
||||||
"message": "Замени све",
|
"message": "Замени све"
|
||||||
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
|
|
||||||
},
|
},
|
||||||
"replaceWith": {
|
"replaceWith": {
|
||||||
"message": "Замени са",
|
"message": "Замени са"
|
||||||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"message": "Претражи",
|
"message": "Претражи"
|
||||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
|
||||||
},
|
},
|
||||||
"searchRegexp": {
|
"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": {
|
"sectionAdd": {
|
||||||
"message": "Додај нови одељак",
|
"message": "Додај нови одељак"
|
||||||
"description": "Label for the button to add a section"
|
|
||||||
},
|
},
|
||||||
"sectionCode": {
|
"sectionCode": {
|
||||||
"message": "Код",
|
"message": "Код"
|
||||||
"description": "Label for the code for a section"
|
|
||||||
},
|
},
|
||||||
"sectionRemove": {
|
"sectionRemove": {
|
||||||
"message": "Уклони одељак",
|
"message": "Уклони одељак"
|
||||||
"description": "Label for the button to remove a section"
|
|
||||||
},
|
},
|
||||||
"styleBadRegexp": {
|
"styleBadRegexp": {
|
||||||
"message": "Регуларни израз је неисправан.",
|
"message": "Регуларни израз је неисправан."
|
||||||
"description": "Validation message for a bad regexp in a style"
|
|
||||||
},
|
},
|
||||||
"styleBeautify": {
|
"styleBeautify": {
|
||||||
"message": "Улепшај",
|
"message": "Улепшај"
|
||||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
|
||||||
},
|
},
|
||||||
"styleCancelEditLabel": {
|
"styleCancelEditLabel": {
|
||||||
"message": "Назад на управљање",
|
"message": "Назад на управљање"
|
||||||
"description": "Label for cancel button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleChangesNotSaved": {
|
"styleChangesNotSaved": {
|
||||||
"message": "Направили сте измене овог стила које нисте сачували.",
|
"message": "Направили сте измене овог стила које нисте сачували."
|
||||||
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
|
|
||||||
},
|
},
|
||||||
"styleEnabledLabel": {
|
"styleEnabledLabel": {
|
||||||
"message": "Омогућено",
|
"message": "Омогућено"
|
||||||
"description": "Label for the enabled state of styles"
|
|
||||||
},
|
},
|
||||||
"styleFromMozillaFormatPrompt": {
|
"styleFromMozillaFormatPrompt": {
|
||||||
"message": "Налепи код у Mozilla формату",
|
"message": "Налепи код у Mozilla формату"
|
||||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
|
||||||
},
|
},
|
||||||
"styleInstall": {
|
"styleInstall": {
|
||||||
"message": "Инсталирати '$stylename$' у Stylus?",
|
"message": "Инсталирати '$stylename$' у Stylus?",
|
||||||
"description": "Confirmation when installing a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -336,28 +254,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Унесите назив",
|
"message": "Унесите назив"
|
||||||
"description": "Error displayed when user saves without providing a name"
|
|
||||||
},
|
},
|
||||||
"styleMozillaFormatHeading": {
|
"styleMozillaFormatHeading": {
|
||||||
"message": "Mozilla формат",
|
"message": "Mozilla формат"
|
||||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
|
||||||
},
|
},
|
||||||
"styleSaveLabel": {
|
"styleSaveLabel": {
|
||||||
"message": "Сачувај",
|
"message": "Сачувај"
|
||||||
"description": "Label for save button for style editing"
|
|
||||||
},
|
},
|
||||||
"styleToMozillaFormatHelp": {
|
"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": {
|
"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": {
|
"styleUpdate": {
|
||||||
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
|
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
|
||||||
"description": "Confirmation when updating a style",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"stylename": {
|
"stylename": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -365,24 +277,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stylusUnavailableForURL": {
|
"stylusUnavailableForURL": {
|
||||||
"message": "Stylus не ради на страницама као што је ова.",
|
"message": "Stylus не ради на страницама као што је ова."
|
||||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
|
||||||
},
|
},
|
||||||
"undo": {
|
"undo": {
|
||||||
"message": "Опозови",
|
"message": "Опозови"
|
||||||
"description": "Button label"
|
|
||||||
},
|
},
|
||||||
"undoGlobal": {
|
"undoGlobal": {
|
||||||
"message": "Опозови (свеобухватно)",
|
"message": "Опозови (свеобухватно)"
|
||||||
"description": "CSS-beautify global Undo button label"
|
|
||||||
},
|
},
|
||||||
"updateAllCheckSucceededNoUpdate": {
|
"updateAllCheckSucceededNoUpdate": {
|
||||||
"message": "Сви стилови су ажурирани.",
|
"message": "Сви стилови су ажурирани."
|
||||||
"description": "Text that displays when an update all check completed and no updates are available"
|
|
||||||
},
|
},
|
||||||
"updateCheckFailBadResponseCode": {
|
"updateCheckFailBadResponseCode": {
|
||||||
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
|
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
|
||||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"code": {
|
"code": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -390,23 +297,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updateCheckFailServerUnreachable": {
|
"updateCheckFailServerUnreachable": {
|
||||||
"message": "Ажурирање није успело: сервер није доступан.",
|
"message": "Ажурирање није успело: сервер није доступан."
|
||||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
|
||||||
},
|
},
|
||||||
"updateCheckSucceededNoUpdate": {
|
"updateCheckSucceededNoUpdate": {
|
||||||
"message": "Стил је ажуриран.",
|
"message": "Стил је ажуриран."
|
||||||
"description": "Text that displays when an update check completed and no update is available"
|
|
||||||
},
|
},
|
||||||
"updateCompleted": {
|
"updateCompleted": {
|
||||||
"message": "Ажурирање је комплетирано.",
|
"message": "Ажурирање је комплетирано."
|
||||||
"description": "Text that displays when an update completed"
|
|
||||||
},
|
},
|
||||||
"writeStyleFor": {
|
"writeStyleFor": {
|
||||||
"message": "Упиши стил за:",
|
"message": "Упиши стил за:"
|
||||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
|
||||||
},
|
},
|
||||||
"writeStyleForURL": {
|
"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,15 +1,12 @@
|
||||||
{
|
{
|
||||||
"addStyleLabel": {
|
"addStyleLabel": {
|
||||||
"message": "క్రొత్త స్టైల్ వ్రాయండి",
|
"message": "క్రొత్త స్టైల్ వ్రాయండి"
|
||||||
"description": "Label for the button to go to the add style page"
|
|
||||||
},
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "చేర్చు",
|
"message": "చేర్చు"
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesDisplay": {
|
"appliesDisplay": {
|
||||||
"message": "వేటికి వర్తిస్తుంది; $applies$",
|
"message": "వేటికి వర్తిస్తుంది; $applies$",
|
||||||
"description": "Text on the manage screen to describe what the style applies to",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"applies": {
|
"applies": {
|
||||||
"content": "$1"
|
"content": "$1"
|
||||||
|
|
@ -17,51 +14,39 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appliesDisplayTruncatedSuffix": {
|
"appliesDisplayTruncatedSuffix": {
|
||||||
"message": "ఇంకా మరిన్ని",
|
"message": "ఇంకా మరిన్ని"
|
||||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
|
||||||
},
|
},
|
||||||
"appliesRemove": {
|
"appliesRemove": {
|
||||||
"message": "తొలగించు",
|
"message": "తొలగించు"
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
|
||||||
},
|
},
|
||||||
"appliesToEverything": {
|
"appliesToEverything": {
|
||||||
"message": "అన్నిటికీ",
|
"message": "అన్నిటికీ"
|
||||||
"description": "Text displayed for styles that apply to all sites"
|
|
||||||
},
|
},
|
||||||
"deleteStyleConfirm": {
|
"deleteStyleConfirm": {
|
||||||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
|
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?"
|
||||||
"description": "Confirmation before deleting a style"
|
|
||||||
},
|
},
|
||||||
"deleteStyleLabel": {
|
"deleteStyleLabel": {
|
||||||
"message": "తొలగించు",
|
"message": "తొలగించు"
|
||||||
"description": "Label for the button to delete a style"
|
|
||||||
},
|
},
|
||||||
"disableStyleLabel": {
|
"disableStyleLabel": {
|
||||||
"message": "అచేతనించు",
|
"message": "అచేతనించు"
|
||||||
"description": "Label for the button to disable a style"
|
|
||||||
},
|
},
|
||||||
"editStyleLabel": {
|
"editStyleLabel": {
|
||||||
"message": "మార్చు",
|
"message": "మార్చు"
|
||||||
"description": "Label for the button to go to the edit style page"
|
|
||||||
},
|
},
|
||||||
"enableStyleLabel": {
|
"enableStyleLabel": {
|
||||||
"message": "చేతనించు",
|
"message": "చేతనించు"
|
||||||
"description": "Label for the button to enable a style"
|
|
||||||
},
|
},
|
||||||
"helpAlt": {
|
"helpAlt": {
|
||||||
"message": "సహాయం",
|
"message": "సహాయం"
|
||||||
"description": "Alternate text for help buttons"
|
|
||||||
},
|
},
|
||||||
"manageHeading": {
|
"manageHeading": {
|
||||||
"message": "స్థాపిత శైలులు",
|
"message": "స్థాపిత శైలులు"
|
||||||
"description": "Heading for the manage page"
|
|
||||||
},
|
},
|
||||||
"manageTitle": {
|
"manageTitle": {
|
||||||
"message": "స్టైలిష్",
|
"message": "స్టైలిష్"
|
||||||
"description": "Title for the manage page"
|
|
||||||
},
|
},
|
||||||
"styleSaveLabel": {
|
"styleSaveLabel": {
|
||||||
"message": "భద్రపరచు",
|
"message": "భద్రపరచు"
|
||||||
"description": "Label for save button for style editing"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
234
_locales/uk/messages.json
Normal file
234
_locales/uk/messages.json
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
{
|
||||||
|
"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": "Резервне копіювання"
|
||||||
|
},
|
||||||
|
"backupMessage": {
|
||||||
|
"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": "Вимкнути"
|
||||||
|
},
|
||||||
|
"findStyles": {
|
||||||
|
"message": "Знайти стилі"
|
||||||
|
},
|
||||||
|
"findStylesForSite": {
|
||||||
|
"message": "Знайти більше стилів для цього сайту"
|
||||||
|
},
|
||||||
|
"findStylesInline": {
|
||||||
|
"message": "і показати тут"
|
||||||
|
},
|
||||||
|
"findStylesInlineTooltip": {
|
||||||
|
"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": "Імпорт стилів закінчено"
|
||||||
|
},
|
||||||
|
"manageFavicons": {
|
||||||
|
"message": "Піктограми для цільових сайтів"
|
||||||
|
},
|
||||||
|
"manageNewUI": {
|
||||||
|
"message": "Новий інтерфейс"
|
||||||
|
},
|
||||||
|
"meta_unknownJSONLiteral": {
|
||||||
|
"message": "Невірний JSON: $literal$не є дійсним літералом JSON",
|
||||||
|
"placeholders": {
|
||||||
|
"literal": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"openManage": {
|
||||||
|
"message": "Менеджер"
|
||||||
|
},
|
||||||
|
"optionsReset": {
|
||||||
|
"message": "Скидання налаштувань до значень за замовчуванням"
|
||||||
|
},
|
||||||
|
"optionsResetButton": {
|
||||||
|
"message": "Скинути параметри"
|
||||||
|
},
|
||||||
|
"optionsSubheading": {
|
||||||
|
"message": "Додатково"
|
||||||
|
},
|
||||||
|
"optionsSyncLogin": {
|
||||||
|
"message": "Логін"
|
||||||
|
},
|
||||||
|
"retrieveBckp": {
|
||||||
|
"message": "Імпорт стилів"
|
||||||
|
},
|
||||||
|
"sectionCode": {
|
||||||
|
"message": "Код"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"message": "Розділи"
|
||||||
|
},
|
||||||
|
"styleBeautify": {
|
||||||
|
"message": "Облагородити"
|
||||||
|
},
|
||||||
|
"styleCancelEditLabel": {
|
||||||
|
"message": "Всі стилі"
|
||||||
|
},
|
||||||
|
"syncErrorRelogin": {
|
||||||
|
"message": "Помилка синхронізації.\nСпробуйте повторно ввійти в налаштування Stylus:\nнатисніть спочатку «від’єднати», а потім «підключити». "
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"message": "Включити/виключити стиль"
|
||||||
|
},
|
||||||
|
"zipStyles": {
|
||||||
|
"message": "Запаковування стилів ... "
|
||||||
|
}
|
||||||
|
}
|
||||||
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,182 +1,26 @@
|
||||||
/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */
|
/* global createWorkerApi */// worker-util.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
importScripts('/js/worker-util.js');
|
/** @namespace BackgroundWorker */
|
||||||
const {loadScript, createAPI} = workerUtil;
|
createWorkerApi({
|
||||||
|
|
||||||
createAPI({
|
async compileUsercss(...args) {
|
||||||
parseMozFormat(arg) {
|
require(['/js/usercss-compiler']); /* global compileUsercss */
|
||||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
return compileUsercss(...args);
|
||||||
return parseMozFormat(arg);
|
|
||||||
},
|
|
||||||
compileUsercss,
|
|
||||||
parseUsercssMeta(text, indexOffset = 0) {
|
|
||||||
loadScript(
|
|
||||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
|
||||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
|
||||||
'/js/meta-parser.js'
|
|
||||||
);
|
|
||||||
return metaParser.parse(text, indexOffset);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
nullifyInvalidVars(vars) {
|
nullifyInvalidVars(vars) {
|
||||||
loadScript(
|
require(['/js/meta-parser']); /* global metaParser */
|
||||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
|
||||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
|
||||||
'/js/meta-parser.js'
|
|
||||||
);
|
|
||||||
return metaParser.nullifyInvalidVars(vars);
|
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',
|
|
||||||
'/vendor-overwrites/colorpicker/colorconverter.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, emptyDocument: preprocessor === 'stylus'}))
|
|
||||||
.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, rgbName) {
|
|
||||||
if (!vars.hasOwnProperty(name)) {
|
|
||||||
if (name.endsWith('-rgb')) {
|
|
||||||
return getValue(name.slice(0, -4), name);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const {type, value} = vars[name];
|
|
||||||
switch (type) {
|
|
||||||
case 'color': {
|
|
||||||
let color = pool.get(rgbName || name);
|
|
||||||
if (color == null) {
|
|
||||||
color = colorConverter.parse(value);
|
|
||||||
if (color) {
|
|
||||||
if (color.type === 'hsl') {
|
|
||||||
color = colorConverter.HSVtoRGB(colorConverter.HSLtoHSV(color));
|
|
||||||
}
|
|
||||||
const {r, g, b} = color;
|
|
||||||
color = rgbName
|
|
||||||
? `${r}, ${g}, ${b}`
|
|
||||||
: `#${(0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
||||||
}
|
|
||||||
// the pool stores `false` for bad colors to differentiate from a yet unknown color
|
|
||||||
pool.set(rgbName || name, color || false);
|
|
||||||
}
|
|
||||||
return color || null;
|
|
||||||
}
|
|
||||||
case 'dropdown':
|
|
||||||
case 'select': // prevent infinite recursion
|
|
||||||
pool.set(name, '');
|
|
||||||
return doReplace(value);
|
|
||||||
}
|
|
||||||
return 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,69 +1,119 @@
|
||||||
/* global download prefs openURL FIREFOX CHROME
|
/* global API msg */// msg.js
|
||||||
URLS ignoreChromeError chromeLocal semverCompare
|
/* global addAPI bgReady */// common.js
|
||||||
styleManager msg navigatorUtil workerUtil contentScripts sync
|
/* global createWorker */// worker-util.js
|
||||||
findExistingTab activateTab isTabReplaceable getActiveTab colorScheme */
|
/* global prefs */
|
||||||
|
/* global styleMan */
|
||||||
|
/* global syncMan */
|
||||||
|
/* global updateMan */
|
||||||
|
/* global usercssMan */
|
||||||
|
/* global uswApi */
|
||||||
|
/* global
|
||||||
|
FIREFOX
|
||||||
|
URLS
|
||||||
|
activateTab
|
||||||
|
download
|
||||||
|
findExistingTab
|
||||||
|
openURL
|
||||||
|
*/ // toolbox.js
|
||||||
|
/* global colorScheme */ // color-scheme.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
//#region API
|
||||||
var backgroundWorker = workerUtil.createWorker({
|
|
||||||
url: '/background/background-worker.js'
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
addAPI(/** @namespace API */ {
|
||||||
var browserCommands, contextMenus;
|
|
||||||
|
|
||||||
// *************************************************************************
|
/** Temporary storage for data needed elsewhere e.g. in a content script */
|
||||||
// browser commands
|
data: ((data = {}) => ({
|
||||||
browserCommands = {
|
del: key => delete data[key],
|
||||||
openManage,
|
get: key => data[key],
|
||||||
openOptions: () => openManage({options: true}),
|
has: key => key in data,
|
||||||
styleDisableAll(info) {
|
pop: key => {
|
||||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
const val = data[key];
|
||||||
|
delete data[key];
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
set: (key, val) => {
|
||||||
|
data[key] = val;
|
||||||
|
},
|
||||||
|
}))(),
|
||||||
|
|
||||||
|
styles: styleMan,
|
||||||
|
sync: syncMan,
|
||||||
|
updater: updateMan,
|
||||||
|
usercss: usercssMan,
|
||||||
|
usw: uswApi,
|
||||||
|
colorScheme,
|
||||||
|
/** @type {BackgroundWorker} */
|
||||||
|
worker: createWorker({url: '/background/background-worker'}),
|
||||||
|
|
||||||
|
download(url, opts) {
|
||||||
|
return typeof url === 'string' && url.startsWith(URLS.uso) &&
|
||||||
|
this.sender.url.startsWith(URLS.uso) &&
|
||||||
|
download(url, opts || {});
|
||||||
},
|
},
|
||||||
reload: () => chrome.runtime.reload(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
|
|
||||||
addInclusion: styleManager.addInclusion,
|
|
||||||
removeInclusion: styleManager.removeInclusion,
|
|
||||||
addExclusion: styleManager.addExclusion,
|
|
||||||
removeExclusion: styleManager.removeExclusion,
|
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
getTabUrlPrefix() {
|
getTabUrlPrefix() {
|
||||||
const {url} = this.sender.tab;
|
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||||
if (url.startsWith(URLS.ownOrigin)) {
|
},
|
||||||
return 'stylus';
|
|
||||||
|
/**
|
||||||
|
* 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 = 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
|
||||||
|
const tab = await openURL({
|
||||||
|
url: `${u}`,
|
||||||
|
currentWindow: null,
|
||||||
|
newWindow: wnd && Object.assign(wndBase, !ffBug && wndPos),
|
||||||
|
});
|
||||||
|
if (ffBug) await browser.windows.update(tab.windowId, wndPos);
|
||||||
|
return tab;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** @returns {Promise<chrome.tabs.Tab>} */
|
||||||
|
async openManage({options = false, search, searchMode} = {}) {
|
||||||
|
let url = chrome.runtime.getURL('manage.html');
|
||||||
|
if (search) {
|
||||||
|
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
|
||||||
}
|
}
|
||||||
return url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
if (options) {
|
||||||
|
url += '#stylus-options';
|
||||||
|
}
|
||||||
|
const tab = await findExistingTab({
|
||||||
|
url,
|
||||||
|
currentWindow: null,
|
||||||
|
ignoreHash: true,
|
||||||
|
ignoreSearch: true,
|
||||||
|
});
|
||||||
|
if (tab) {
|
||||||
|
await activateTab(tab);
|
||||||
|
if (url !== (tab.pendingUrl || tab.url)) {
|
||||||
|
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
return openURL({url, ignoreExisting: true}).then(activateTab); // activateTab unminimizes the window
|
||||||
},
|
},
|
||||||
|
|
||||||
download(msg) {
|
/**
|
||||||
delete msg.method;
|
* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
|
||||||
return download(msg.url, msg);
|
* 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
|
||||||
parseCss({code}) {
|
* popup (in Chrome at least) and preventing the sendMessage code from running
|
||||||
return backgroundWorker.parseMozFormat({code});
|
* @returns {Promise<chrome.tabs.Tab>}
|
||||||
},
|
*/
|
||||||
getPrefs: prefs.getAll,
|
|
||||||
|
|
||||||
openEditor,
|
|
||||||
|
|
||||||
/* 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 */
|
|
||||||
async openURL(opts) {
|
async openURL(opts) {
|
||||||
const tab = await openURL(opts);
|
const tab = await openURL(opts);
|
||||||
if (opts.message) {
|
if (opts.message) {
|
||||||
|
|
@ -84,259 +134,62 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
optionsCustomizeHotkeys() {
|
prefs: {
|
||||||
return browserCommands.openOptions()
|
getValues: () => prefs.__values, // will be deepCopy'd by apiHandler
|
||||||
.then(() => new Promise(resolve => setTimeout(resolve, 500)))
|
set: prefs.set,
|
||||||
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSystemPreferDark: colorScheme.updateSystemPreferDark,
|
|
||||||
|
|
||||||
syncStart: sync.start,
|
|
||||||
syncStop: sync.stop,
|
|
||||||
syncNow: sync.syncNow,
|
|
||||||
getSyncStatus: sync.getStatus,
|
|
||||||
syncLogin: sync.login,
|
|
||||||
|
|
||||||
openManage
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// *************************************************************************
|
//#endregion
|
||||||
// register all listeners
|
//#region Events
|
||||||
msg.on(onRuntimeMessage);
|
|
||||||
|
|
||||||
// tell apply.js to refresh styles for non-committed navigation
|
const browserCommands = {
|
||||||
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
|
openManage: () => API.openManage(),
|
||||||
if (type !== 'committed') {
|
openOptions: () => API.openManage({options: true}),
|
||||||
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId})
|
reload: () => chrome.runtime.reload(),
|
||||||
.catch(msg.ignoreError);
|
styleDisableAll(info) {
|
||||||
}
|
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||||
});
|
|
||||||
|
|
||||||
if (FIREFOX) {
|
|
||||||
// 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]());
|
|
||||||
}
|
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
|
||||||
// save install type: "admin", "development", "normal", "sideload" or "other"
|
|
||||||
// "normal" = addon installed from webstore
|
|
||||||
chrome.management.getSelf(info => {
|
|
||||||
localStorage.installType = info.installType;
|
|
||||||
if (reason === 'install' && info.installType === 'development' && chrome.contextMenus) {
|
|
||||||
createContextMenus(['reload']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (reason !== 'update') return;
|
|
||||||
// translations may change
|
|
||||||
localStorage.L10N = JSON.stringify({
|
|
||||||
browserUIlanguage: chrome.i18n.getUILanguage(),
|
|
||||||
});
|
|
||||||
// themes may change
|
|
||||||
delete localStorage.codeMirrorThemes;
|
|
||||||
// inline search cache for USO is not needed anymore, TODO: remove this by the middle of 2021
|
|
||||||
if (semverCompare(previousVersion, '1.5.13') <= 0) {
|
|
||||||
setTimeout(async () => {
|
|
||||||
const del = Object.keys(await chromeLocal.get())
|
|
||||||
.filter(key => key.startsWith('usoSearchCache'));
|
|
||||||
if (del.length) chromeLocal.remove(del);
|
|
||||||
}, 15e3);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
'open-options': {
|
|
||||||
title: 'openOptions',
|
|
||||||
click: browserCommands.openOptions,
|
|
||||||
},
|
|
||||||
'reload': {
|
|
||||||
presentIf: () => localStorage.installType === 'development',
|
|
||||||
title: 'reload',
|
|
||||||
click: browserCommands.reload,
|
|
||||||
},
|
|
||||||
'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');
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createContextMenus(ids) {
|
if (chrome.commands) {
|
||||||
for (const id of ids) {
|
chrome.commands.onCommand.addListener(id => browserCommands[id]());
|
||||||
let item = contextMenus[id];
|
}
|
||||||
if (item.presentIf && !item.presentIf()) {
|
|
||||||
continue;
|
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
||||||
|
if (reason === 'update') {
|
||||||
|
const [a, b, c] = (previousVersion || '').split('.');
|
||||||
|
if (a <= 1 && b <= 5 && c <= 13) { // 1.5.13
|
||||||
|
require(['/background/remove-unused-storage']);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if (chrome.contextMenus) {
|
msg.on((msg, sender) => {
|
||||||
// circumvent the bug with disabling check marks in Chrome 62-64
|
if (msg.method === 'invokeAPI') {
|
||||||
const toggleCheckmark = CHROME >= 62 && CHROME <= 64 ?
|
let res = msg.path.reduce((res, name) => res && res[name], API);
|
||||||
(id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) :
|
if (!res) throw new Error(`Unknown API.${msg.path.join('.')}`);
|
||||||
((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError));
|
res = res.apply({msg, sender}, msg.args);
|
||||||
|
return res === undefined ? null : res;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditor(params) {
|
//#endregion
|
||||||
/* Open the editor. Activate if it is already opened
|
|
||||||
|
|
||||||
params: {
|
Promise.all([
|
||||||
id?: Number,
|
bgReady.styles,
|
||||||
domain?: String,
|
/* These are loaded conditionally.
|
||||||
'url-prefix'?: String
|
Each item uses `require` individually so IDE can jump to the source and track usage. */
|
||||||
}
|
FIREFOX &&
|
||||||
*/
|
require(['/background/style-via-api']),
|
||||||
const u = new URL(chrome.runtime.getURL('edit.html'));
|
FIREFOX && ((browser.commands || {}).update) &&
|
||||||
u.search = new URLSearchParams(params);
|
require(['/background/browser-cmd-hotkeys']),
|
||||||
return openURL({
|
!FIREFOX &&
|
||||||
url: `${u}`,
|
require(['/background/content-scripts']),
|
||||||
currentWindow: null,
|
chrome.contextMenus &&
|
||||||
newWindow: prefs.get('openEditInWindow') && Object.assign({},
|
require(['/background/context-menus']),
|
||||||
prefs.get('openEditInWindow.popup') && {type: 'popup'},
|
]).then(() => {
|
||||||
prefs.get('windowPosition')),
|
bgReady._resolveAll();
|
||||||
});
|
msg.isBgReady = true;
|
||||||
}
|
msg.broadcast({method: 'backgroundReady'});
|
||||||
|
});
|
||||||
function openManage({options = false, search} = {}) {
|
|
||||||
let url = chrome.runtime.getURL('manage.html');
|
|
||||||
if (search) {
|
|
||||||
url += `?search=${encodeURIComponent(search)}`;
|
|
||||||
}
|
|
||||||
if (options) {
|
|
||||||
url += '#stylus-options';
|
|
||||||
}
|
|
||||||
return findExistingTab({
|
|
||||||
url,
|
|
||||||
currentWindow: null,
|
|
||||||
ignoreHash: true,
|
|
||||||
ignoreSearch: true
|
|
||||||
})
|
|
||||||
.then(tab => {
|
|
||||||
if (tab) {
|
|
||||||
return Promise.all([
|
|
||||||
activateTab(tab),
|
|
||||||
(tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url})
|
|
||||||
.catch(console.error)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return getActiveTab().then(tab => {
|
|
||||||
if (isTabReplaceable(tab, url)) {
|
|
||||||
return activateTab(tab, {url});
|
|
||||||
}
|
|
||||||
return browser.tabs.create({url});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
22
background/browser-cmd-hotkeys.js
Normal file
22
background/browser-cmd-hotkeys.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* 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) {}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -36,7 +36,7 @@ const colorScheme = (() => {
|
||||||
}
|
}
|
||||||
chrome.alarms.create(key, {
|
chrome.alarms.create(key, {
|
||||||
when: date.getTime(),
|
when: date.getTime(),
|
||||||
periodInMinutes: 24 * 60
|
periodInMinutes: 24 * 60,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
31
background/common.js
Normal file
31
background/common.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* global API */// msg.js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common stuff that's loaded first so it's immediately available to all background scripts
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* exported
|
||||||
|
addAPI
|
||||||
|
bgReady
|
||||||
|
compareRevision
|
||||||
|
*/
|
||||||
|
|
||||||
|
const bgReady = {};
|
||||||
|
bgReady.styles = new Promise(r => (bgReady._resolveStyles = r));
|
||||||
|
bgReady.all = new Promise(r => (bgReady._resolveAll = r));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareRevision(rev1, rev2) {
|
||||||
|
return rev1 - rev2;
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
/* global msg ignoreChromeError URLS */
|
/* global bgReady */// common.js
|
||||||
/* exported contentScripts */
|
/* global msg */
|
||||||
|
/* global URLS ignoreChromeError */// toolbox.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const contentScripts = (() => {
|
/*
|
||||||
|
Reinject content scripts when the extension is reloaded/updated.
|
||||||
|
Not used in Firefox as it reinjects automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
bgReady.all.then(() => {
|
||||||
const NTP = 'chrome://newtab/';
|
const NTP = 'chrome://newtab/';
|
||||||
const ALL_URLS = '<all_urls>';
|
const ALL_URLS = '<all_urls>';
|
||||||
const SCRIPTS = chrome.runtime.getManifest().content_scripts;
|
const SCRIPTS = chrome.runtime.getManifest().content_scripts;
|
||||||
// expand * as .*?
|
// expand * as .*?
|
||||||
const wildcardAsRegExp = (s, flags) => new RegExp(
|
const wildcardAsRegExp = (s, flags) => new RegExp(
|
||||||
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
|
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
|
||||||
.replace(/\*/g, '.*?'), flags);
|
.replace(/\*/g, '.*?'), flags);
|
||||||
for (const cs of SCRIPTS) {
|
for (const cs of SCRIPTS) {
|
||||||
cs.matches = cs.matches.map(m => (
|
cs.matches = cs.matches.map(m => (
|
||||||
m === ALL_URLS ? m : wildcardAsRegExp(m)
|
m === ALL_URLS ? m : wildcardAsRegExp(m)
|
||||||
|
|
@ -18,21 +24,7 @@ const contentScripts = (() => {
|
||||||
const busyTabs = new Set();
|
const busyTabs = new Set();
|
||||||
let busyTabsTimer;
|
let busyTabsTimer;
|
||||||
|
|
||||||
// expose version on greasyfork/sleazyfork 1) info page and 2) code page
|
setTimeout(injectToAllTabs);
|
||||||
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},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
return {injectToTab, injectToAllTabs};
|
|
||||||
|
|
||||||
function injectToTab({url, tabId, frameId = null}) {
|
function injectToTab({url, tabId, frameId = null}) {
|
||||||
for (const script of SCRIPTS) {
|
for (const script of SCRIPTS) {
|
||||||
|
|
@ -57,7 +49,7 @@ const contentScripts = (() => {
|
||||||
const options = {
|
const options = {
|
||||||
runAt: script.run_at,
|
runAt: script.run_at,
|
||||||
allFrames: script.all_frames,
|
allFrames: script.all_frames,
|
||||||
matchAboutBlank: script.match_about_blank
|
matchAboutBlank: script.match_about_blank,
|
||||||
};
|
};
|
||||||
if (frameId !== null) {
|
if (frameId !== null) {
|
||||||
options.allFrames = false;
|
options.allFrames = false;
|
||||||
|
|
@ -80,7 +72,7 @@ const contentScripts = (() => {
|
||||||
} else {
|
} else {
|
||||||
injectToTab({
|
injectToTab({
|
||||||
url: tab.pendingUrl || tab.url,
|
url: tab.pendingUrl || tab.url,
|
||||||
tabId: tab.id
|
tabId: tab.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,4 +114,4 @@ const contentScripts = (() => {
|
||||||
function onBusyTabRemoved(tabId) {
|
function onBusyTabRemoved(tabId) {
|
||||||
trackBusyTab(tabId, false);
|
trackBusyTab(tabId, false);
|
||||||
}
|
}
|
||||||
})();
|
});
|
||||||
|
|
|
||||||
101
background/context-menus.js
Normal file
101
background/context-menus.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/* global browserCommands */// background.js
|
||||||
|
/* global msg */
|
||||||
|
/* global prefs */
|
||||||
|
/* global CHROME FIREFOX URLS ignoreChromeError */// toolbox.js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const 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,
|
||||||
|
},
|
||||||
|
'open-options': {
|
||||||
|
title: 'openOptions',
|
||||||
|
click: browserCommands.openOptions,
|
||||||
|
},
|
||||||
|
'reload': {
|
||||||
|
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
|
||||||
|
title: 'reload',
|
||||||
|
click: browserCommands.reload,
|
||||||
|
},
|
||||||
|
'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')
|
||||||
|
.catch(msg.ignoreError);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// "Delete" item in context menu for browsers that don't have it
|
||||||
|
if (CHROME &&
|
||||||
|
// looking at the end of UA string
|
||||||
|
/(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) &&
|
||||||
|
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
|
||||||
|
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) {
|
||||||
|
prefs.__defaults['editor.contextDelete'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(contextMenus);
|
||||||
|
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'),
|
||||||
|
CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
|
||||||
|
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && prefs.knownKeys.includes(id)),
|
||||||
|
togglePresence);
|
||||||
|
|
||||||
|
createContextMenus(keys);
|
||||||
|
|
||||||
|
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
||||||
|
contextMenus[info.menuItemId].click(info, tab));
|
||||||
|
|
||||||
|
async function createContextMenus(ids) {
|
||||||
|
for (const id of ids) {
|
||||||
|
let item = contextMenus[id];
|
||||||
|
if (item.presentIf && !await 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePresence(id, checked) {
|
||||||
|
if (checked) {
|
||||||
|
createContextMenus([id]);
|
||||||
|
} else {
|
||||||
|
chrome.contextMenus.remove(id, ignoreChromeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,67 +1,66 @@
|
||||||
/* global chromeLocal */
|
/* global chromeLocal */// storage-util.js
|
||||||
/* exported createChromeStorageDB */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/* exported createChromeStorageDB */
|
||||||
function createChromeStorageDB() {
|
function createChromeStorageDB() {
|
||||||
let INC;
|
let INC;
|
||||||
|
|
||||||
const PREFIX = 'style-';
|
const PREFIX = 'style-';
|
||||||
const METHODS = {
|
const METHODS = {
|
||||||
|
|
||||||
|
delete(id) {
|
||||||
|
return chromeLocal.remove(PREFIX + id);
|
||||||
|
},
|
||||||
|
|
||||||
// FIXME: we don't use this method at all. Should we remove this?
|
// FIXME: we don't use this method at all. Should we remove this?
|
||||||
get: id => chromeLocal.getValue(PREFIX + id),
|
get(id) {
|
||||||
put: obj =>
|
return chromeLocal.getValue(PREFIX + id);
|
||||||
// FIXME: should we clone the object?
|
},
|
||||||
Promise.resolve(!obj.id && prepareInc().then(() => Object.assign(obj, {id: INC++})))
|
|
||||||
.then(() => chromeLocal.setValue(PREFIX + obj.id, obj))
|
async getAll() {
|
||||||
.then(() => obj.id),
|
const all = await chromeLocal.get();
|
||||||
putMany: items => prepareInc()
|
if (!INC) prepareInc(all);
|
||||||
.then(() =>
|
return Object.entries(all)
|
||||||
chromeLocal.set(items.reduce((data, item) => {
|
.map(([key, val]) => key.startsWith(PREFIX) && Number(key.slice(PREFIX.length)) && val)
|
||||||
if (!item.id) item.id = INC++;
|
.filter(Boolean);
|
||||||
data[PREFIX + item.id] = item;
|
},
|
||||||
return data;
|
|
||||||
}, {})))
|
async put(item) {
|
||||||
.then(() => items.map(i => i.id)),
|
if (!item.id) {
|
||||||
delete: id => chromeLocal.remove(PREFIX + id),
|
if (!INC) await prepareInc();
|
||||||
getAll: () => chromeLocal.get()
|
item.id = INC++;
|
||||||
.then(result => {
|
}
|
||||||
const output = [];
|
await chromeLocal.setValue(PREFIX + item.id, item);
|
||||||
for (const key in result) {
|
return item.id;
|
||||||
if (key.startsWith(PREFIX) && Number(key.slice(PREFIX.length))) {
|
},
|
||||||
output.push(result[key]);
|
|
||||||
}
|
async putMany(items) {
|
||||||
|
const data = {};
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.id) {
|
||||||
|
if (!INC) await prepareInc();
|
||||||
|
item.id = INC++;
|
||||||
}
|
}
|
||||||
return output;
|
data[PREFIX + item.id] = item;
|
||||||
})
|
}
|
||||||
|
await chromeLocal.set(data);
|
||||||
|
return items.map(_ => _.id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {exec};
|
async function prepareInc(data) {
|
||||||
|
INC = 1;
|
||||||
function exec(method, ...args) {
|
for (const key in data || await chromeLocal.get()) {
|
||||||
if (METHODS[method]) {
|
if (key.startsWith(PREFIX)) {
|
||||||
return METHODS[method](...args)
|
const id = Number(key.slice(PREFIX.length));
|
||||||
.then(result => {
|
if (id >= INC) {
|
||||||
if (method === 'putMany' && result.map) {
|
INC = id + 1;
|
||||||
return result.map(r => ({target: {result: r}}));
|
|
||||||
}
|
|
||||||
return {target: {result}};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(`unknown DB method ${method}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareInc() {
|
|
||||||
if (INC) return Promise.resolve();
|
|
||||||
return chromeLocal.get().then(result => {
|
|
||||||
INC = 1;
|
|
||||||
for (const key in result) {
|
|
||||||
if (key.startsWith(PREFIX)) {
|
|
||||||
const id = Number(key.slice(PREFIX.length));
|
|
||||||
if (id >= INC) {
|
|
||||||
INC = id + 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return function dbExecChromeStorage(method, ...args) {
|
||||||
|
return METHODS[method](...args);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
/* global chromeLocal workerUtil createChromeStorageDB */
|
/* global chromeLocal */// storage-util.js
|
||||||
/* exported db */
|
/* global cloneError */// worker-util.js
|
||||||
/*
|
|
||||||
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';
|
'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 = (() => {
|
const db = (() => {
|
||||||
const DATABASE = 'stylish';
|
const DATABASE = 'stylish';
|
||||||
const STORE = 'styles';
|
const STORE = 'styles';
|
||||||
|
|
@ -24,52 +25,34 @@ const db = (() => {
|
||||||
async function tryUsingIndexedDB() {
|
async function tryUsingIndexedDB() {
|
||||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
// 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
|
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||||
// for reliablility and in localStorage for fast synchronous access
|
// note that accessing indexedDB may throw, https://github.com/openstyles/stylus/issues/615
|
||||||
// (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') {
|
if (typeof indexedDB === 'undefined') {
|
||||||
throw new Error('indexedDB is undefined');
|
throw new Error('indexedDB is undefined');
|
||||||
}
|
}
|
||||||
switch (await getFallback()) {
|
switch (await chromeLocal.getValue(FALLBACK)) {
|
||||||
case true: throw null;
|
case true: throw null;
|
||||||
case false: break;
|
case false: break;
|
||||||
default: await testDB();
|
default: await testDB();
|
||||||
}
|
}
|
||||||
return useIndexedDB();
|
chromeLocal.setValue(FALLBACK, false);
|
||||||
}
|
return dbExecIndexedDB;
|
||||||
|
|
||||||
async function getFallback() {
|
|
||||||
return localStorage[FALLBACK] === 'true' ? true :
|
|
||||||
localStorage[FALLBACK] === 'false' ? false :
|
|
||||||
chromeLocal.getValue(FALLBACK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testDB() {
|
async function testDB() {
|
||||||
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
|
|
||||||
// throws if result is null
|
|
||||||
e = e.target.result[0];
|
|
||||||
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
|
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
|
||||||
await dbExecIndexedDB('put', {id});
|
await dbExecIndexedDB('put', {id});
|
||||||
e = await dbExecIndexedDB('get', id);
|
const e = await dbExecIndexedDB('get', id);
|
||||||
// throws if result or id is null
|
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
|
||||||
await dbExecIndexedDB('delete', e.target.result.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useChromeStorage(err) {
|
async function useChromeStorage(err) {
|
||||||
chromeLocal.setValue(FALLBACK, true);
|
chromeLocal.setValue(FALLBACK, true);
|
||||||
if (err) {
|
if (err) {
|
||||||
chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
|
chromeLocal.setValue(FALLBACK + 'Reason', cloneError(err));
|
||||||
console.warn('Failed to access indexedDB. Switched to storage API.', err);
|
console.warn('Failed to access indexedDB. Switched to storage API.', err);
|
||||||
}
|
}
|
||||||
localStorage[FALLBACK] = 'true';
|
await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
|
||||||
return createChromeStorageDB().exec;
|
return createChromeStorageDB();
|
||||||
}
|
|
||||||
|
|
||||||
function useIndexedDB() {
|
|
||||||
chromeLocal.setValue(FALLBACK, false);
|
|
||||||
localStorage[FALLBACK] = 'false';
|
|
||||||
return dbExecIndexedDB;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dbExecIndexedDB(method, ...args) {
|
async function dbExecIndexedDB(method, ...args) {
|
||||||
|
|
@ -81,8 +64,9 @@ const db = (() => {
|
||||||
|
|
||||||
function storeRequest(store, method, ...args) {
|
function storeRequest(store, method, ...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
/** @type {IDBRequest} */
|
||||||
const request = store[method](...args);
|
const request = store[method](...args);
|
||||||
request.onsuccess = resolve;
|
request.onsuccess = () => resolve(request.result);
|
||||||
request.onerror = reject;
|
request.onerror = reject;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,41 @@
|
||||||
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */
|
/* global API */// msg.js
|
||||||
/* exported iconManager */
|
/* global addAPI bgReady */// common.js
|
||||||
|
/* global prefs */
|
||||||
|
/* global tabMan */
|
||||||
|
/* global CHROME FIREFOX VIVALDI debounce ignoreChromeError */// toolbox.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const iconManager = (() => {
|
/* exported iconMan */
|
||||||
|
const iconMan = (() => {
|
||||||
const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
|
const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
|
||||||
const staleBadges = new Set();
|
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 && navigator.userAgent.includes('Android');
|
||||||
|
|
||||||
prefs.subscribe([
|
// https://github.com/openstyles/stylus/issues/335
|
||||||
'disableAll',
|
let hasCanvas = FIREFOX_ANDROID ? false : loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
|
||||||
'badgeDisabled',
|
.then(({data}) => (hasCanvas = data.some(b => b !== 255)));
|
||||||
'badgeNormal',
|
|
||||||
], () => debounce(refreshIconBadgeColor));
|
|
||||||
|
|
||||||
prefs.subscribe([
|
addAPI(/** @namespace API */ {
|
||||||
'show-badge'
|
/**
|
||||||
], () => debounce(refreshAllIconsBadgeText));
|
* @param {(number|string)[]} styleIds
|
||||||
|
* @param {boolean} [lazyBadge=false] preventing flicker during page load
|
||||||
prefs.subscribe([
|
*/
|
||||||
'disableAll',
|
|
||||||
'iconset',
|
|
||||||
], () => debounce(refreshAllIcons));
|
|
||||||
|
|
||||||
prefs.initializing.then(() => {
|
|
||||||
refreshIconBadgeColor();
|
|
||||||
refreshAllIconsBadgeText();
|
|
||||||
refreshAllIcons();
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(API_METHODS, {
|
|
||||||
/** @param {(number|string)[]} styleIds
|
|
||||||
* @param {boolean} [lazyBadge=false] preventing flicker during page load */
|
|
||||||
updateIconBadge(styleIds, {lazyBadge} = {}) {
|
updateIconBadge(styleIds, {lazyBadge} = {}) {
|
||||||
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
|
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
|
||||||
const {frameId, tab: {id: tabId}} = this.sender;
|
const {frameId, tab: {id: tabId}} = this.sender;
|
||||||
const value = styleIds.length ? styleIds.map(Number) : undefined;
|
const value = styleIds.length ? styleIds.map(Number) : undefined;
|
||||||
tabManager.set(tabId, 'styleIds', frameId, value);
|
tabMan.set(tabId, 'styleIds', frameId, value);
|
||||||
debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
|
debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
|
||||||
staleBadges.add(tabId);
|
staleBadges.add(tabId);
|
||||||
if (!frameId) refreshIcon(tabId, true);
|
if (!frameId) refreshIcon(tabId, true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigatorUtil.onCommitted(({tabId, frameId}) => {
|
chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
|
||||||
if (!frameId) tabManager.set(tabId, 'styleIds', undefined);
|
if (!frameId) tabMan.set(tabId, 'styleIds', undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
chrome.runtime.onConnect.addListener(port => {
|
||||||
|
|
@ -51,15 +44,54 @@ const iconManager = (() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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}) {
|
function onPortDisconnected({sender}) {
|
||||||
if (tabManager.get(sender.tab.id, 'styleIds')) {
|
if (tabMan.get(sender.tab.id, 'styleIds')) {
|
||||||
API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true});
|
API.updateIconBadge.call({sender}, [], {lazyBadge: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshIconBadgeText(tabId) {
|
function refreshIconBadgeText(tabId) {
|
||||||
|
if (badgeOvr.text) return;
|
||||||
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
|
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
|
||||||
iconUtil.setBadgeText({tabId, text});
|
setBadgeText({tabId, text});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconName(hasStyles = false) {
|
function getIconName(hasStyles = false) {
|
||||||
|
|
@ -69,17 +101,17 @@ const iconManager = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshIcon(tabId, force = false) {
|
function refreshIcon(tabId, force = false) {
|
||||||
const oldIcon = tabManager.get(tabId, 'icon');
|
const oldIcon = tabMan.get(tabId, 'icon');
|
||||||
const newIcon = getIconName(tabManager.get(tabId, 'styleIds', 0));
|
const newIcon = getIconName(tabMan.get(tabId, 'styleIds', 0));
|
||||||
// (changing the icon only for the main page, frameId = 0)
|
// (changing the icon only for the main page, frameId = 0)
|
||||||
|
|
||||||
if (!force && oldIcon === newIcon) {
|
if (!force && oldIcon === newIcon) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tabManager.set(tabId, 'icon', newIcon);
|
tabMan.set(tabId, 'icon', newIcon);
|
||||||
iconUtil.setIcon({
|
setIcon({
|
||||||
path: getIconPath(newIcon),
|
path: getIconPath(newIcon),
|
||||||
tabId
|
tabId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,33 +128,55 @@ const iconManager = (() => {
|
||||||
/** @return {number | ''} */
|
/** @return {number | ''} */
|
||||||
function getStyleCount(tabId) {
|
function getStyleCount(tabId) {
|
||||||
const allIds = new Set();
|
const allIds = new Set();
|
||||||
const data = tabManager.get(tabId, 'styleIds') || {};
|
const data = tabMan.get(tabId, 'styleIds') || {};
|
||||||
Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
|
Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
|
||||||
return allIds.size || '';
|
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() {
|
function refreshGlobalIcon() {
|
||||||
iconUtil.setIcon({
|
setIcon({
|
||||||
path: getIconPath(getIconName())
|
path: getIconPath(getIconName()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshIconBadgeColor() {
|
function refreshIconBadgeColor() {
|
||||||
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
|
setBadgeBackgroundColor({
|
||||||
iconUtil.setBadgeBackgroundColor({
|
color: badgeOvr.color ||
|
||||||
color
|
prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshAllIcons() {
|
function refreshAllIcons() {
|
||||||
for (const tabId of tabManager.list()) {
|
for (const tabId of tabMan.list()) {
|
||||||
refreshIcon(tabId);
|
refreshIcon(tabId);
|
||||||
}
|
}
|
||||||
refreshGlobalIcon();
|
refreshGlobalIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshAllIconsBadgeText() {
|
function refreshAllIconsBadgeText() {
|
||||||
for (const tabId of tabManager.list()) {
|
for (const tabId of tabMan.list()) {
|
||||||
refreshIconBadgeText(tabId);
|
refreshIconBadgeText(tabId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,4 +187,40 @@ const iconManager = (() => {
|
||||||
}
|
}
|
||||||
staleBadges.clear();
|
staleBadges.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeCall(method, data) {
|
||||||
|
const {browserAction = {}} = chrome;
|
||||||
|
const fn = browserAction[method];
|
||||||
|
if (fn) {
|
||||||
|
try {
|
||||||
|
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
||||||
|
fn.call(browserAction, data, ignoreChromeError);
|
||||||
|
} catch (e) {
|
||||||
|
// FIXME: skip pre-rendered tabs?
|
||||||
|
fn.call(browserAction, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {chrome.browserAction.TabIconDetails} data */
|
||||||
|
async function setIcon(data) {
|
||||||
|
if (hasCanvas === true || await hasCanvas) {
|
||||||
|
data.imageData = {};
|
||||||
|
for (const [key, url] of Object.entries(data.path)) {
|
||||||
|
data.imageData[key] = imageDataCache.get(url) || await loadImage(url);
|
||||||
|
}
|
||||||
|
delete data.path;
|
||||||
|
}
|
||||||
|
safeCall('setIcon', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {chrome.browserAction.BadgeTextDetails} data */
|
||||||
|
function setBadgeText(data) {
|
||||||
|
safeCall('setBadgeText', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {chrome.browserAction.BadgeBackgroundColorDetails} data */
|
||||||
|
function setBadgeBackgroundColor(data) {
|
||||||
|
safeCall('setBadgeBackgroundColor', data);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
/* 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
103
background/navigation-manager.js
Normal file
103
background/navigation-manager.js
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* global CHROME FIREFOX URLS deepEqual ignoreChromeError */// toolbox.js
|
||||||
|
/* global bgReady */// common.js
|
||||||
|
/* global msg */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* exported navMan */
|
||||||
|
const navMan = (() => {
|
||||||
|
const listeners = new Set();
|
||||||
|
let prevData = {};
|
||||||
|
|
||||||
|
chrome.webNavigation.onCommitted.addListener(onNavigation.bind('committed'));
|
||||||
|
chrome.webNavigation.onHistoryStateUpdated.addListener(onFakeNavigation.bind('history'));
|
||||||
|
chrome.webNavigation.onReferenceFragmentUpdated.addListener(onFakeNavigation.bind('hash'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** @param {function(data: Object, type: ('committed'|'history'|'hash'))} fn */
|
||||||
|
onUrlChange(fn) {
|
||||||
|
listeners.add(fn);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {string} type */
|
||||||
|
async function onNavigation(data) {
|
||||||
|
if (CHROME && data.timeStamp === prevData.timeStamp && deepEqual(data, prevData)) {
|
||||||
|
return; // Chrome bug: listener is called twice with identical data
|
||||||
|
}
|
||||||
|
prevData = data;
|
||||||
|
if (CHROME &&
|
||||||
|
URLS.chromeProtectsNTP &&
|
||||||
|
data.url.startsWith('https://www.google.') &&
|
||||||
|
data.url.includes('/_/chrome/newtab?')) {
|
||||||
|
// Modern Chrome switched to WebUI NTP so this is obsolete, but there may be exceptions
|
||||||
|
// TODO: investigate, and maybe use a separate listener for CHROME <= ver
|
||||||
|
const tab = await browser.tabs.get(data.tabId);
|
||||||
|
const url = tab.pendingUrl || tab.url;
|
||||||
|
if (url === 'chrome://newtab/') {
|
||||||
|
data.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listeners.forEach(fn => fn(data, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @this {string} type */
|
||||||
|
function onFakeNavigation(data) {
|
||||||
|
const {url, frameId} = data;
|
||||||
|
onNavigation.call(this, data);
|
||||||
|
msg.sendTab(data.tabId, {method: 'urlChanged', url}, {frameId})
|
||||||
|
.catch(msg.ignoreError);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
bgReady.all.then(() => {
|
||||||
|
/*
|
||||||
|
* Expose style version on greasyfork/sleazyfork 1) info page and 2) code page
|
||||||
|
* Not using manifest.json as adding a content script disables the extension on update.
|
||||||
|
*/
|
||||||
|
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
|
||||||
|
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
|
||||||
|
chrome.tabs.executeScript(tabId, {
|
||||||
|
file: '/content/install-hook-greasyfork.js',
|
||||||
|
runAt: 'document_start',
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
url: [
|
||||||
|
{hostEquals: 'greasyfork.org', urlMatches},
|
||||||
|
{hostEquals: 'sleazyfork.org', urlMatches},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Removes the Get Stylus button on style pages.
|
||||||
|
* Not using manifest.json as adding a content script disables the extension on update.
|
||||||
|
*/
|
||||||
|
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
|
||||||
|
chrome.tabs.executeScript(tabId, {
|
||||||
|
file: '/content/install-hook-userstylesworld.js',
|
||||||
|
runAt: 'document_start',
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
url: [
|
||||||
|
{hostEquals: 'userstyles.world'},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
* FF misses some about:blank iframes so we inject our content script explicitly
|
||||||
|
*/
|
||||||
|
if (FIREFOX) {
|
||||||
|
chrome.webNavigation.onDOMContentLoaded.addListener(async ({tabId, frameId}) => {
|
||||||
|
if (frameId &&
|
||||||
|
!await msg.sendTab(tabId, {method: 'ping'}, {frameId}).catch(ignoreChromeError)) {
|
||||||
|
for (const file of chrome.runtime.getManifest().content_scripts[0].js) {
|
||||||
|
chrome.tabs.executeScript(tabId, {
|
||||||
|
frameId,
|
||||||
|
file,
|
||||||
|
matchAboutBlank: true,
|
||||||
|
}, ignoreChromeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
url: [{urlEquals: 'about:blank'}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/* global CHROME URLS */
|
|
||||||
/* exported navigatorUtil */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const navigatorUtil = (() => {
|
|
||||||
const handler = {
|
|
||||||
urlChange: null
|
|
||||||
};
|
|
||||||
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 browser.tabs.get(data.tabId)
|
|
||||||
.then(tab => {
|
|
||||||
const url = tab.pendingUrl || tab.url;
|
|
||||||
if (url === 'chrome://newtab/') {
|
|
||||||
data.url = 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]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
'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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
15
background/remove-unused-storage.js
Normal file
15
background/remove-unused-storage.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* global chromeLocal */// storage-util.js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Removing unused stuff from storage on extension update
|
||||||
|
// TODO: delete this by the middle of 2021
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.clear();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const del = Object.keys(await chromeLocal.get())
|
||||||
|
.filter(key => key.startsWith('usoSearchCache'));
|
||||||
|
if (del.length) chromeLocal.remove(del);
|
||||||
|
}, 15e3);
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
/* 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 = part === 'name' ? style.customName || style.name : 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
108
background/style-search-db.js
Normal file
108
background/style-search-db.js
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
/* 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,7 +1,14 @@
|
||||||
/* global API_METHODS styleManager CHROME prefs */
|
/* global API */// msg.js
|
||||||
|
/* global addAPI */// common.js
|
||||||
|
/* global isEmptyObj */// toolbox.js
|
||||||
|
/* global prefs */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
API_METHODS.styleViaAPI = !CHROME && (() => {
|
/**
|
||||||
|
* Uses chrome.tabs.insertCSS
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
styleApply,
|
styleApply,
|
||||||
styleDeleted,
|
styleDeleted,
|
||||||
|
|
@ -11,25 +18,25 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
prefChanged,
|
prefChanged,
|
||||||
updateCount,
|
updateCount,
|
||||||
};
|
};
|
||||||
const NOP = Promise.resolve(new Error('NOP'));
|
const NOP = new Error('NOP');
|
||||||
const onError = () => {};
|
const onError = () => {};
|
||||||
|
|
||||||
/* <tabId>: Object
|
/* <tabId>: Object
|
||||||
<frameId>: Object
|
<frameId>: Object
|
||||||
url: String, non-enumerable
|
url: String, non-enumerable
|
||||||
<styleId>: Array of strings
|
<styleId>: Array of strings
|
||||||
section code */
|
section code */
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
let observingTabs = false;
|
let observingTabs = false;
|
||||||
|
|
||||||
return function (request) {
|
addAPI(/** @namespace API */ {
|
||||||
const action = ACTIONS[request.method];
|
async styleViaAPI(request) {
|
||||||
return !action ? NOP :
|
try {
|
||||||
action(request, this.sender)
|
const fn = ACTIONS[request.method];
|
||||||
.catch(onError)
|
return fn ? fn(request, this.sender) : NOP;
|
||||||
.then(maybeToggleObserver);
|
} catch (e) {}
|
||||||
};
|
maybeToggleObserver();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function updateCount(request, sender) {
|
function updateCount(request, sender) {
|
||||||
const {tab, frameId} = sender;
|
const {tab, frameId} = sender;
|
||||||
|
|
@ -37,7 +44,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
throw new Error('we do not count styles for frames');
|
throw new Error('we do not count styles for frames');
|
||||||
}
|
}
|
||||||
const {frameStyles} = getCachedData(tab.id, frameId);
|
const {frameStyles} = getCachedData(tab.id, frameId);
|
||||||
API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles));
|
API.updateIconBadge.call({sender}, Object.keys(frameStyles));
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
||||||
|
|
@ -48,7 +55,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
||||||
return NOP;
|
return NOP;
|
||||||
}
|
}
|
||||||
return styleManager.getSectionsByUrl(url, id).then(sections => {
|
return API.styles.getSectionsByUrl(url, id).then(sections => {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
for (const section of Object.values(sections)) {
|
for (const section of Object.values(sections)) {
|
||||||
const styleId = section.id;
|
const styleId = section.id;
|
||||||
|
|
@ -125,7 +132,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
}
|
}
|
||||||
const {tab, frameId} = sender;
|
const {tab, frameId} = sender;
|
||||||
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
|
||||||
if (isEmpty(frameStyles)) {
|
if (isEmptyObj(frameStyles)) {
|
||||||
return NOP;
|
return NOP;
|
||||||
}
|
}
|
||||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
|
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
|
||||||
|
|
@ -162,7 +169,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
const tabFrames = cache.get(tabId);
|
const tabFrames = cache.get(tabId);
|
||||||
if (tabFrames && frameId in tabFrames) {
|
if (tabFrames && frameId in tabFrames) {
|
||||||
delete tabFrames[frameId];
|
delete tabFrames[frameId];
|
||||||
if (isEmpty(tabFrames)) {
|
if (isEmptyObj(tabFrames)) {
|
||||||
onTabRemoved(tabId);
|
onTabRemoved(tabId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,9 +185,9 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
|
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
|
||||||
if (isEmpty(frameStyles)) {
|
if (isEmptyObj(frameStyles)) {
|
||||||
delete tabFrames[frameId];
|
delete tabFrames[frameId];
|
||||||
if (isEmpty(tabFrames)) {
|
if (isEmptyObj(tabFrames)) {
|
||||||
cache.delete(tabId);
|
cache.delete(tabId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -223,11 +230,4 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
|
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
|
||||||
.catch(onError);
|
.catch(onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmpty(obj) {
|
|
||||||
for (const k in obj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
152
background/style-via-webrequest.js
Normal file
152
background/style-via-webrequest.js
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
/* global API */// msg.js
|
||||||
|
/* global CHROME 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'}]});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function req2key(req) {
|
||||||
|
return req.tabId + ':' + req.frameId;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
/* global API CHROME prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
|
||||||
CHROME && (async () => {
|
|
||||||
const prefId = 'styleViaXhr';
|
|
||||||
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
|
|
||||||
const stylesToPass = {};
|
|
||||||
|
|
||||||
await prefs.initializing;
|
|
||||||
toggle(prefId, prefs.get(prefId));
|
|
||||||
prefs.subscribe([prefId], toggle);
|
|
||||||
|
|
||||||
function toggle(key, value) {
|
|
||||||
if (!chrome.declarativeContent) { // not yet granted in options page
|
|
||||||
value = false;
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
const reqFilter = {
|
|
||||||
urls: ['<all_urls>'],
|
|
||||||
types: ['main_frame', 'sub_frame'],
|
|
||||||
};
|
|
||||||
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(passStyles, reqFilter, [
|
|
||||||
'blocking',
|
|
||||||
'responseHeaders',
|
|
||||||
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
|
|
||||||
].filter(Boolean));
|
|
||||||
} else {
|
|
||||||
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
|
|
||||||
chrome.webRequest.onHeadersReceived.removeListener(passStyles);
|
|
||||||
}
|
|
||||||
if (!chrome.declarativeContent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.declarativeContent.onPageChanged.removeRules([prefId], async () => {
|
|
||||||
if (!value) return;
|
|
||||||
chrome.declarativeContent.onPageChanged.addRules([{
|
|
||||||
id: prefId,
|
|
||||||
conditions: [
|
|
||||||
new chrome.declarativeContent.PageStateMatcher({
|
|
||||||
pageUrl: {urlContains: ':'},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
new chrome.declarativeContent.RequestContentScript({
|
|
||||||
allFrames: true,
|
|
||||||
// This runs earlier than document_start
|
|
||||||
js: chrome.runtime.getManifest().content_scripts[0].js,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
|
|
||||||
function prepareStyles(req) {
|
|
||||||
API.getSectionsByUrl(req.url).then(sections => {
|
|
||||||
const str = JSON.stringify(sections);
|
|
||||||
if (str !== '{}') {
|
|
||||||
stylesToPass[req.requestId] = URL.createObjectURL(new Blob([str])).slice(blobUrlPrefix.length);
|
|
||||||
setTimeout(cleanUp, 600e3, req.requestId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
|
|
||||||
function passStyles(req) {
|
|
||||||
const blobId = stylesToPass[req.requestId];
|
|
||||||
if (blobId) {
|
|
||||||
const {responseHeaders} = req;
|
|
||||||
responseHeaders.push({
|
|
||||||
name: 'Set-Cookie',
|
|
||||||
value: `${chrome.runtime.id}=${prefs.get('disableAll') ? 1 : 0}${blobId}`,
|
|
||||||
});
|
|
||||||
return {responseHeaders};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanUp(key) {
|
|
||||||
const blobId = stylesToPass[key];
|
|
||||||
delete stylesToPass[key];
|
|
||||||
if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
264
background/sync-manager.js
Normal file
264
background/sync-manager.js
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
/* global API msg */// msg.js
|
||||||
|
/* global chromeLocal */// storage-util.js
|
||||||
|
/* global compareRevision */// common.js
|
||||||
|
/* global iconMan */
|
||||||
|
/* global prefs */
|
||||||
|
/* global tokenMan */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const syncMan = (() => {
|
||||||
|
//#region Init
|
||||||
|
|
||||||
|
const SYNC_DELAY = 1; // minutes
|
||||||
|
const SYNC_INTERVAL = 30; // minutes
|
||||||
|
const SYNC_LOCK_RETRIES = 10; // number of retries before the error is reported for scheduled sync
|
||||||
|
const STATES = Object.freeze({
|
||||||
|
connected: 'connected',
|
||||||
|
connecting: 'connecting',
|
||||||
|
disconnected: 'disconnected',
|
||||||
|
disconnecting: 'disconnecting',
|
||||||
|
});
|
||||||
|
const STORAGE_KEY = 'sync/state/';
|
||||||
|
const status = /** @namespace SyncManager.Status */ {
|
||||||
|
STATES,
|
||||||
|
state: STATES.disconnected,
|
||||||
|
syncing: false,
|
||||||
|
progress: null,
|
||||||
|
currentDriveName: null,
|
||||||
|
errorMessage: null,
|
||||||
|
login: false,
|
||||||
|
lockRetries: 0,
|
||||||
|
};
|
||||||
|
let lastError = null;
|
||||||
|
let ctrl;
|
||||||
|
let currentDrive;
|
||||||
|
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
||||||
|
let ready = prefs.ready.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({isScheduled: true});
|
||||||
|
const retrying = status.lockRetries / SYNC_LOCK_RETRIES * Math.random();
|
||||||
|
schedule(SYNC_DELAY + SYNC_INTERVAL * (retrying || 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//#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 put(...args) {
|
||||||
|
if (ready.then) await ready;
|
||||||
|
if (!currentDrive) return;
|
||||||
|
schedule();
|
||||||
|
return ctrl.put(...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async start(name, fromPref = false) {
|
||||||
|
if (ready.then) await ready;
|
||||||
|
if (!ctrl) await initController();
|
||||||
|
|
||||||
|
if (currentDrive) return;
|
||||||
|
currentDrive = getDrive(name);
|
||||||
|
ctrl.use(currentDrive);
|
||||||
|
|
||||||
|
status.state = STATES.connecting;
|
||||||
|
status.currentDriveName = currentDrive.name;
|
||||||
|
emitStatusChange();
|
||||||
|
|
||||||
|
if (fromPref) {
|
||||||
|
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({isScheduled} = {}) {
|
||||||
|
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) {
|
||||||
|
status.errorMessage = err.message;
|
||||||
|
lastError = err;
|
||||||
|
if (isScheduled &&
|
||||||
|
err.code === 409 &&
|
||||||
|
++status.lockRetries <= SYNC_LOCK_RETRIES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isGrantError(err)) {
|
||||||
|
status.login = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.lockRetries = 0;
|
||||||
|
emitStatusChange();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region Utils
|
||||||
|
|
||||||
|
async function initController() {
|
||||||
|
await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
|
||||||
|
ctrl = dbToCloud.dbToCloud({
|
||||||
|
onGet(id) {
|
||||||
|
return API.styles.getByUUID(id);
|
||||||
|
},
|
||||||
|
onPut(doc) {
|
||||||
|
return API.styles.putByUUID(doc);
|
||||||
|
},
|
||||||
|
onDelete(id, rev) {
|
||||||
|
return API.styles.deleteByUUID(id, rev);
|
||||||
|
},
|
||||||
|
async onFirstSync() {
|
||||||
|
for (const i of 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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')
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrive(name) {
|
||||||
|
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
|
||||||
|
return dbToCloud.drive[name]({
|
||||||
|
getAccessToken: () => tokenMan.getToken(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error(`unknown cloud name: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule(delay = SYNC_DELAY) {
|
||||||
|
chrome.alarms.create('syncNow', {
|
||||||
|
delayInMinutes: delay, // fractional values are supported
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
})();
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
|
|
||||||
/* exported sync */
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const sync = (() => {
|
|
||||||
const SYNC_DELAY = 1; // minutes
|
|
||||||
const SYNC_INTERVAL = 30; // minutes
|
|
||||||
|
|
||||||
const status = {
|
|
||||||
state: 'disconnected',
|
|
||||||
syncing: false,
|
|
||||||
progress: null,
|
|
||||||
currentDriveName: null,
|
|
||||||
errorMessage: null,
|
|
||||||
login: false
|
|
||||||
};
|
|
||||||
let currentDrive;
|
|
||||||
const ctrl = dbToCloud.dbToCloud({
|
|
||||||
onGet(id) {
|
|
||||||
return styleManager.getByUUID(id);
|
|
||||||
},
|
|
||||||
onPut(doc) {
|
|
||||||
return styleManager.putByUUID(doc);
|
|
||||||
},
|
|
||||||
onDelete(id, rev) {
|
|
||||||
return styleManager.deleteByUUID(id, rev);
|
|
||||||
},
|
|
||||||
onFirstSync() {
|
|
||||||
return styleManager.getAllStyles()
|
|
||||||
.then(styles => {
|
|
||||||
styles.forEach(i => ctrl.put(i._id, i._rev));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onProgress,
|
|
||||||
compareRevision(a, b) {
|
|
||||||
return styleManager.compareRevision(a, b);
|
|
||||||
},
|
|
||||||
getState(drive) {
|
|
||||||
const key = `sync/state/${drive.name}`;
|
|
||||||
return chromeLocal.get(key)
|
|
||||||
.then(obj => obj[key]);
|
|
||||||
},
|
|
||||||
setState(drive, state) {
|
|
||||||
const key = `sync/state/${drive.name}`;
|
|
||||||
return chromeLocal.set({
|
|
||||||
[key]: state
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const initializing = prefs.initializing.then(() => {
|
|
||||||
prefs.subscribe(['sync.enabled'], onPrefChange);
|
|
||||||
onPrefChange(null, prefs.get('sync.enabled'));
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener(info => {
|
|
||||||
if (info.name === 'syncNow') {
|
|
||||||
syncNow().catch(console.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.assign({
|
|
||||||
getStatus: () => status
|
|
||||||
}, ensurePrepared({
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
put: (...args) => {
|
|
||||||
if (!currentDrive) return;
|
|
||||||
schedule();
|
|
||||||
return ctrl.put(...args);
|
|
||||||
},
|
|
||||||
delete: (...args) => {
|
|
||||||
if (!currentDrive) return;
|
|
||||||
schedule();
|
|
||||||
return ctrl.delete(...args);
|
|
||||||
},
|
|
||||||
syncNow,
|
|
||||||
login
|
|
||||||
}));
|
|
||||||
|
|
||||||
function ensurePrepared(obj) {
|
|
||||||
return Object.entries(obj).reduce((o, [key, fn]) => {
|
|
||||||
o[key] = (...args) =>
|
|
||||||
initializing.then(() => fn(...args));
|
|
||||||
return o;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
function schedule(delay = SYNC_DELAY) {
|
|
||||||
chrome.alarms.create('syncNow', {
|
|
||||||
delayInMinutes: delay,
|
|
||||||
periodInMinutes: SYNC_INTERVAL
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPrefChange(key, value) {
|
|
||||||
if (value === 'none') {
|
|
||||||
stop().catch(console.error);
|
|
||||||
} else {
|
|
||||||
start(value, true).catch(console.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function withFinally(p, cleanup) {
|
|
||||||
return p.then(
|
|
||||||
result => {
|
|
||||||
cleanup(undefined, result);
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
cleanup(err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncNow() {
|
|
||||||
if (!currentDrive) {
|
|
||||||
return Promise.reject(new Error('cannot sync when disconnected'));
|
|
||||||
}
|
|
||||||
return withFinally(
|
|
||||||
(ctrl.isInit() ? ctrl.syncNow() : ctrl.start())
|
|
||||||
.catch(handle401Error),
|
|
||||||
err => {
|
|
||||||
status.errorMessage = err ? err.message : null;
|
|
||||||
emitStatusChange();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle401Error(err) {
|
|
||||||
if (err.code === 401) {
|
|
||||||
return tokenManager.revokeToken(currentDrive.name)
|
|
||||||
.catch(console.error)
|
|
||||||
.then(() => {
|
|
||||||
status.login = false;
|
|
||||||
emitStatusChange();
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (/User interaction required|Requires user interaction/i.test(err.message)) {
|
|
||||||
status.login = false;
|
|
||||||
emitStatusChange();
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitStatusChange() {
|
|
||||||
msg.broadcastExtension({method: 'syncStatusUpdate', status});
|
|
||||||
}
|
|
||||||
|
|
||||||
function login(name = prefs.get('sync.enabled')) {
|
|
||||||
return tokenManager.getToken(name, true)
|
|
||||||
.catch(err => {
|
|
||||||
if (/Authorization page could not be loaded/i.test(err.message)) {
|
|
||||||
// FIXME: Chrome always fails at the first login so we try again
|
|
||||||
return tokenManager.getToken(name);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
status.login = true;
|
|
||||||
emitStatusChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function start(name, fromPref = false) {
|
|
||||||
if (currentDrive) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
currentDrive = getDrive(name);
|
|
||||||
ctrl.use(currentDrive);
|
|
||||||
status.state = 'connecting';
|
|
||||||
status.currentDriveName = currentDrive.name;
|
|
||||||
status.login = true;
|
|
||||||
emitStatusChange();
|
|
||||||
return withFinally(
|
|
||||||
(fromPref ? Promise.resolve() : login(name))
|
|
||||||
.catch(handle401Error)
|
|
||||||
.then(() => syncNow()),
|
|
||||||
err => {
|
|
||||||
status.errorMessage = err ? err.message : null;
|
|
||||||
// FIXME: should we move this logic to options.js?
|
|
||||||
if (err && !fromPref) {
|
|
||||||
console.error(err);
|
|
||||||
return stop();
|
|
||||||
}
|
|
||||||
prefs.set('sync.enabled', name);
|
|
||||||
schedule(SYNC_INTERVAL);
|
|
||||||
status.state = 'connected';
|
|
||||||
emitStatusChange();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDrive(name) {
|
|
||||||
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
|
|
||||||
return dbToCloud.drive[name]({
|
|
||||||
getAccessToken: () => tokenManager.getToken(name)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new Error(`unknown cloud name: ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop() {
|
|
||||||
if (!currentDrive) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
chrome.alarms.clear('syncNow');
|
|
||||||
status.state = 'disconnecting';
|
|
||||||
emitStatusChange();
|
|
||||||
return withFinally(
|
|
||||||
ctrl.stop()
|
|
||||||
.then(() => tokenManager.revokeToken(currentDrive.name))
|
|
||||||
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
|
|
||||||
() => {
|
|
||||||
currentDrive = null;
|
|
||||||
prefs.set('sync.enabled', 'none');
|
|
||||||
status.state = 'disconnected';
|
|
||||||
status.currentDriveName = null;
|
|
||||||
status.login = false;
|
|
||||||
emitStatusChange();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,32 +1,37 @@
|
||||||
/* global navigatorUtil */
|
/* global bgReady */// common.js
|
||||||
/* exported tabManager */
|
/* global navMan */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const tabManager = (() => {
|
const tabMan = (() => {
|
||||||
const listeners = [];
|
const listeners = new Set();
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
chrome.tabs.onRemoved.addListener(tabId => cache.delete(tabId));
|
chrome.tabs.onRemoved.addListener(tabId => cache.delete(tabId));
|
||||||
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
|
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
|
||||||
navigatorUtil.onUrlChange(({tabId, frameId, url}) => {
|
|
||||||
if (frameId) return;
|
bgReady.all.then(() => {
|
||||||
const oldUrl = tabManager.get(tabId, 'url');
|
navMan.onUrlChange(({tabId, frameId, url}) => {
|
||||||
tabManager.set(tabId, 'url', url);
|
const oldUrl = !frameId && tabMan.get(tabId, 'url', frameId);
|
||||||
for (const fn of listeners) {
|
tabMan.set(tabId, 'url', frameId, url);
|
||||||
try {
|
if (frameId) return;
|
||||||
fn({tabId, url, oldUrl});
|
for (const fn of listeners) {
|
||||||
} catch (err) {
|
try {
|
||||||
console.error(err);
|
fn({tabId, url, oldUrl});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onUpdate(fn) {
|
onUpdate(fn) {
|
||||||
listeners.push(fn);
|
listeners.add(fn);
|
||||||
},
|
},
|
||||||
|
|
||||||
get(tabId, ...keys) {
|
get(tabId, ...keys) {
|
||||||
return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
|
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
|
* 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', 123) will set tabId's meta to {foo: 123},
|
||||||
|
|
@ -47,8 +52,11 @@ const tabManager = (() => {
|
||||||
meta[lastKey] = value;
|
meta[lastKey] = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** @returns {IterableIterator<number>} */
|
||||||
list() {
|
list() {
|
||||||
return cache.keys();
|
return cache.keys();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */
|
/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js
|
||||||
/* exported tokenManager */
|
/* global chromeLocal */// storage-util.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const tokenManager = (() => {
|
/* exported tokenMan */
|
||||||
promisifyChrome({
|
const tokenMan = (() => {
|
||||||
'windows': ['create', 'update', 'remove'],
|
|
||||||
'tabs': ['create', 'update', 'remove']
|
|
||||||
});
|
|
||||||
const AUTH = {
|
const AUTH = {
|
||||||
dropbox: {
|
dropbox: {
|
||||||
flow: 'token',
|
flow: 'token',
|
||||||
|
|
@ -17,9 +14,9 @@ const tokenManager = (() => {
|
||||||
fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
|
fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
google: {
|
google: {
|
||||||
flow: 'code',
|
flow: 'code',
|
||||||
|
|
@ -31,14 +28,14 @@ const tokenManager = (() => {
|
||||||
// tokens for multiple machines.
|
// tokens for multiple machines.
|
||||||
// https://stackoverflow.com/q/18519185
|
// https://stackoverflow.com/q/18519185
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
prompt: 'consent'
|
prompt: 'consent',
|
||||||
},
|
},
|
||||||
tokenURL: 'https://oauth2.googleapis.com/token',
|
tokenURL: 'https://oauth2.googleapis.com/token',
|
||||||
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
|
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
|
||||||
revoke: token => {
|
revoke: token => {
|
||||||
const params = {token};
|
const params = {token};
|
||||||
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
|
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
onedrive: {
|
onedrive: {
|
||||||
flow: 'code',
|
flow: 'code',
|
||||||
|
|
@ -49,102 +46,103 @@ const tokenManager = (() => {
|
||||||
redirect_uri: FIREFOX ?
|
redirect_uri: FIREFOX ?
|
||||||
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
|
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
|
||||||
'https://' + location.hostname + '.chromiumapp.org/',
|
'https://' + location.hostname + '.chromiumapp.org/',
|
||||||
scopes: ['Files.ReadWrite.AppFolder', 'offline_access']
|
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 NETWORK_LATENCY = 30; // seconds
|
||||||
|
|
||||||
return {getToken, revokeToken, getClientId, buildKeys};
|
let alwaysUseTab = FIREFOX ? false : null;
|
||||||
|
|
||||||
function getClientId(name) {
|
return {
|
||||||
return AUTH[name].clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildKeys(name) {
|
buildKeys(name, hooks) {
|
||||||
const k = {
|
const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
|
||||||
TOKEN: `secure/token/${name}/token`,
|
const k = {
|
||||||
EXPIRE: `secure/token/${name}/expire`,
|
TOKEN: `${prefix}token`,
|
||||||
REFRESH: `secure/token/${name}/refresh`,
|
EXPIRE: `${prefix}expire`,
|
||||||
};
|
REFRESH: `${prefix}refresh`,
|
||||||
k.LIST = Object.values(k);
|
};
|
||||||
return k;
|
k.LIST = Object.values(k);
|
||||||
}
|
return k;
|
||||||
|
},
|
||||||
|
|
||||||
function getToken(name, interactive) {
|
getClientId(name) {
|
||||||
const k = buildKeys(name);
|
return AUTH[name].clientId;
|
||||||
return chromeLocal.get(k.LIST)
|
},
|
||||||
.then(obj => {
|
|
||||||
if (!obj[k.TOKEN]) {
|
async getToken(name, interactive, hooks) {
|
||||||
return authUser(name, k, interactive);
|
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]) {
|
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
|
||||||
return obj[k.TOKEN];
|
return obj[k.TOKEN];
|
||||||
}
|
}
|
||||||
if (obj[k.REFRESH]) {
|
if (obj[k.REFRESH]) {
|
||||||
return refreshToken(name, k, obj)
|
return refreshToken(name, k, obj);
|
||||||
.catch(err => {
|
|
||||||
if (err.code === 401) {
|
|
||||||
return authUser(name, k, interactive);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return authUser(name, k, interactive);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function revokeToken(name) {
|
|
||||||
const provider = AUTH[name];
|
|
||||||
const k = buildKeys(name);
|
|
||||||
return revoke()
|
|
||||||
.then(() => chromeLocal.remove(k.LIST));
|
|
||||||
|
|
||||||
function revoke() {
|
|
||||||
if (!provider.revoke) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
return chromeLocal.get(k.TOKEN)
|
if (!interactive) {
|
||||||
.then(obj => {
|
throw new Error(`Invalid token: ${name}`);
|
||||||
if (obj[k.TOKEN]) {
|
}
|
||||||
return provider.revoke(obj[k.TOKEN]);
|
return authUser(k, name, interactive, hooks);
|
||||||
}
|
},
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshToken(name, k, obj) {
|
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]) {
|
if (!obj[k.REFRESH]) {
|
||||||
return Promise.reject(new Error('no refresh token'));
|
throw new Error('No refresh token');
|
||||||
}
|
}
|
||||||
const provider = AUTH[name];
|
const provider = AUTH[name];
|
||||||
const body = {
|
const body = {
|
||||||
client_id: provider.clientId,
|
client_id: provider.clientId,
|
||||||
refresh_token: obj[k.REFRESH],
|
refresh_token: obj[k.REFRESH],
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
scope: provider.scopes.join(' ')
|
scope: provider.scopes.join(' '),
|
||||||
};
|
};
|
||||||
if (provider.clientSecret) {
|
if (provider.clientSecret) {
|
||||||
body.client_secret = provider.clientSecret;
|
body.client_secret = provider.clientSecret;
|
||||||
}
|
}
|
||||||
return postQuery(provider.tokenURL, body)
|
const result = await postQuery(provider.tokenURL, body);
|
||||||
.then(result => {
|
if (!result.refresh_token) {
|
||||||
if (!result.refresh_token) {
|
// reuse old refresh token
|
||||||
// reuse old refresh token
|
result.refresh_token = obj[k.REFRESH];
|
||||||
result.refresh_token = obj[k.REFRESH];
|
}
|
||||||
}
|
return handleTokenResult(result, k);
|
||||||
return handleTokenResult(result, k);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function authUser(name, k, interactive = false) {
|
async function authUser(keys, name, interactive = false, hooks = null) {
|
||||||
|
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min']);
|
||||||
|
/* global webextLaunchWebAuthFlow */
|
||||||
const provider = AUTH[name];
|
const provider = AUTH[name];
|
||||||
const state = Math.random().toFixed(8).slice(2);
|
const state = Math.random().toFixed(8).slice(2);
|
||||||
const query = {
|
const query = {
|
||||||
response_type: provider.flow,
|
response_type: provider.flow,
|
||||||
client_id: provider.clientId,
|
client_id: provider.clientId,
|
||||||
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
|
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
|
||||||
state
|
state,
|
||||||
};
|
};
|
||||||
if (provider.scopes) {
|
if (provider.scopes) {
|
||||||
query.scope = provider.scopes.join(' ');
|
query.scope = provider.scopes.join(' ');
|
||||||
|
|
@ -152,71 +150,111 @@ const tokenManager = (() => {
|
||||||
if (provider.authQuery) {
|
if (provider.authQuery) {
|
||||||
Object.assign(query, 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 url = `${provider.authURL}?${new URLSearchParams(query)}`;
|
||||||
return webextLaunchWebAuthFlow({
|
const width = Math.min(screen.availWidth - 100, 800);
|
||||||
|
const height = Math.min(screen.availHeight - 100, 800);
|
||||||
|
const wnd = await browser.windows.getLastFocused();
|
||||||
|
const finalUrl = await webextLaunchWebAuthFlow({
|
||||||
url,
|
url,
|
||||||
|
alwaysUseTab,
|
||||||
interactive,
|
interactive,
|
||||||
redirect_uri: query.redirect_uri
|
redirect_uri: query.redirect_uri,
|
||||||
})
|
windowOptions: Object.assign({
|
||||||
.then(url => {
|
state: 'normal',
|
||||||
const params = new URLSearchParams(
|
width,
|
||||||
provider.flow === 'token' ?
|
height,
|
||||||
new URL(url).hash.slice(1) :
|
}, wnd.state !== 'minimized' && {
|
||||||
new URL(url).search.slice(1)
|
// Center the popup to the current window
|
||||||
);
|
top: Math.ceil(wnd.top + (wnd.height - width) / 2),
|
||||||
if (params.get('state') !== state) {
|
left: Math.ceil(wnd.left + (wnd.width - width) / 2),
|
||||||
throw new Error(`unexpected state: ${params.get('state')}, expected: ${state}`);
|
}),
|
||||||
}
|
});
|
||||||
if (provider.flow === 'token') {
|
const params = new URLSearchParams(
|
||||||
const obj = {};
|
provider.flow === 'token' ?
|
||||||
for (const [key, value] of params.entries()) {
|
new URL(finalUrl).hash.slice(1) :
|
||||||
obj[key] = value;
|
new URL(finalUrl).search.slice(1)
|
||||||
}
|
);
|
||||||
return obj;
|
if (params.get('state') !== state) {
|
||||||
}
|
throw new Error(`Unexpected state: ${params.get('state')}, expected: ${state}`);
|
||||||
const code = params.get('code');
|
}
|
||||||
const body = {
|
let result;
|
||||||
code,
|
if (provider.flow === 'token') {
|
||||||
grant_type: 'authorization_code',
|
const obj = {};
|
||||||
client_id: provider.clientId,
|
for (const [key, value] of params) {
|
||||||
redirect_uri: query.redirect_uri
|
obj[key] = value;
|
||||||
};
|
}
|
||||||
if (provider.clientSecret) {
|
result = obj;
|
||||||
body.client_secret = provider.clientSecret;
|
} else {
|
||||||
}
|
const code = params.get('code');
|
||||||
return postQuery(provider.tokenURL, body);
|
const body = {
|
||||||
})
|
code,
|
||||||
.then(result => handleTokenResult(result, k));
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTokenResult(result, k) {
|
async function handleTokenResult(result, k) {
|
||||||
return chromeLocal.set({
|
await chromeLocal.set({
|
||||||
[k.TOKEN]: result.access_token,
|
[k.TOKEN]: result.access_token,
|
||||||
[k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined,
|
[k.EXPIRE]: result.expires_in
|
||||||
[k.REFRESH]: result.refresh_token
|
? Date.now() + (result.expires_in - NETWORK_LATENCY) * 1000
|
||||||
})
|
: undefined,
|
||||||
.then(() => result.access_token);
|
[k.REFRESH]: result.refresh_token,
|
||||||
|
});
|
||||||
|
return result.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function postQuery(url, body) {
|
async function postQuery(url, body) {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: body ? new URLSearchParams(body) : null,
|
body: body ? new URLSearchParams(body) : null,
|
||||||
};
|
};
|
||||||
return fetch(url, options)
|
const r = await fetch(url, options);
|
||||||
.then(r => {
|
if (r.ok) {
|
||||||
if (r.ok) {
|
return r.json();
|
||||||
return r.json();
|
}
|
||||||
}
|
const text = await r.text();
|
||||||
return r.text()
|
const err = new Error(`Failed to fetch (${r.status}): ${text}`);
|
||||||
.then(body => {
|
err.code = r.status;
|
||||||
const err = new Error(`failed to fetch (${r.status}): ${body}`);
|
throw err;
|
||||||
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) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
349
background/update-manager.js
Normal file
349
background/update-manager.js
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
/* global API */// msg.js
|
||||||
|
/* global RX_META URLS debounce download ignoreChromeError */// toolbox.js
|
||||||
|
/* global calcStyleDigest styleJSONseemsValid 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 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 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);
|
||||||
|
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 = true
|
||||||
|
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 ucd = style.usercssData;
|
||||||
|
let res, state;
|
||||||
|
try {
|
||||||
|
await checkIfEdited();
|
||||||
|
res = {
|
||||||
|
style: await (ucd ? 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} (${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 url = URLS.makeUsoArchiveCodeUrl(style.md5Url.match(/\d+/)[0]);
|
||||||
|
const req = await tryDownload(url, RH_ETAG).catch(() => null);
|
||||||
|
if (req) {
|
||||||
|
return updateToUSOArchive(url, req);
|
||||||
|
}
|
||||||
|
const md5 = await tryDownload(style.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);
|
||||||
|
}
|
||||||
|
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||||
|
if (!styleJSONseemsValid(json)) {
|
||||||
|
return Promise.reject(STATES.ERROR_JSON);
|
||||||
|
}
|
||||||
|
// USO may not provide a correctly updated originalMd5 (#555)
|
||||||
|
json.originalMd5 = md5;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateToUSOArchive(url, req) {
|
||||||
|
const m2 = getUsoEmbeddedMeta(req.response);
|
||||||
|
if (m2) {
|
||||||
|
url = (await m2).updateUrl;
|
||||||
|
req = await tryDownload(url, RH_ETAG);
|
||||||
|
}
|
||||||
|
const json = await API.usercss.buildMeta({
|
||||||
|
id,
|
||||||
|
etag: req.headers.etag,
|
||||||
|
md5Url: null,
|
||||||
|
originalMd5: null,
|
||||||
|
sourceCode: req.response,
|
||||||
|
updateUrl: url,
|
||||||
|
url: URLS.extractUsoArchiveInstallUrl(url),
|
||||||
|
});
|
||||||
|
const varUrlValues = style.updateUrl.split('?')[1];
|
||||||
|
const varData = json.usercssData.vars;
|
||||||
|
if (varUrlValues && varData) {
|
||||||
|
const IK = 'ik-';
|
||||||
|
const IK_LEN = IK.length;
|
||||||
|
for (let [key, val] of new URLSearchParams(varUrlValues)) {
|
||||||
|
if (!key.startsWith(IK)) continue;
|
||||||
|
key = key.slice(IK_LEN);
|
||||||
|
const varDef = varData[key];
|
||||||
|
if (!varDef) continue;
|
||||||
|
if (varDef.options) {
|
||||||
|
let sel = val.startsWith(IK) && getVarOptByName(varDef, val.slice(IK_LEN));
|
||||||
|
if (!sel) {
|
||||||
|
key += '-custom';
|
||||||
|
sel = getVarOptByName(varDef, key + '-dropdown');
|
||||||
|
if (sel) varData[key].value = val;
|
||||||
|
}
|
||||||
|
if (sel) varDef.value = sel.name;
|
||||||
|
} else {
|
||||||
|
varDef.value = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return API.usercss.buildCode(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUsercss() {
|
||||||
|
let oldVer = ucd.version;
|
||||||
|
let {etag: oldEtag, updateUrl} = style;
|
||||||
|
let m2 = URLS.extractUsoArchiveId(updateUrl) && getUsoEmbeddedMeta();
|
||||||
|
if (m2 && (m2 = await m2).updateUrl) {
|
||||||
|
updateUrl = m2.updateUrl;
|
||||||
|
oldVer = m2.usercssData.version || '0';
|
||||||
|
oldEtag = '';
|
||||||
|
}
|
||||||
|
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 : 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.exec('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 {
|
||||||
|
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 = URLS.extractUsoArchiveId(style.updateUrl) &&
|
||||||
|
style.usercssData.version.match(RX_DATE2VER);
|
||||||
|
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 m = code.includes('@updateURL') && code.replace(RX_META, '').match(RX_META);
|
||||||
|
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVarOptByName(varDef, name) {
|
||||||
|
return varDef.options.find(o => o.name === name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,276 +0,0 @@
|
||||||
/* 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.customName || 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.customName || 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;
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
/* global API_METHODS usercss styleManager deepCopy */
|
|
||||||
/* exported usercssHelper */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const usercssHelper = (() => {
|
|
||||||
API_METHODS.installUsercss = installUsercss;
|
|
||||||
API_METHODS.editSaveUsercss = editSaveUsercss;
|
|
||||||
API_METHODS.configUsercssVars = configUsercssVars;
|
|
||||||
|
|
||||||
API_METHODS.buildUsercss = build;
|
|
||||||
API_METHODS.findUsercss = find;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,66 +1,107 @@
|
||||||
/* global API_METHODS openURL download URLS tabManager */
|
/* global RX_META URLS download openURL */// toolbox.js
|
||||||
|
/* global addAPI bgReady */// common.js
|
||||||
|
/* global tabMan */// msg.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
bgReady.all.then(() => {
|
||||||
const installCodeCache = {};
|
const installCodeCache = {};
|
||||||
const clearInstallCode = url => delete installCodeCache[url];
|
|
||||||
const isContentTypeText = type => /^text\/(css|plain)(;.*?)?$/i.test(type);
|
|
||||||
|
|
||||||
// in Firefox we have to use a content script to read file://
|
addAPI(/** @namespace API */ {
|
||||||
const fileLoader = !chrome.app && (
|
usercss: {
|
||||||
async tabId =>
|
getInstallCode(url) {
|
||||||
(await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]);
|
// 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const urlLoader =
|
// `glob`: pathname match pattern for webRequest
|
||||||
async (tabId, url) => (
|
// `rx`: pathname regex to verify the URL really looks like a raw usercss
|
||||||
url.startsWith('file:') ||
|
const maybeDistro = {
|
||||||
tabManager.get(tabId, isContentTypeText.name) ||
|
// https://github.com/StylishThemes/GitHub-Dark/raw/master/github-dark.user.css
|
||||||
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
|
'github.com': {
|
||||||
) && download(url);
|
glob: '/*/raw/*',
|
||||||
|
rx: /^\/[^/]+\/[^/]+\/raw\/[^/]+\/[^/]+?\.user\.(css|styl)$/,
|
||||||
API_METHODS.getUsercssInstallCode = url => {
|
},
|
||||||
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
|
// https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/github-dark.user.css
|
||||||
const {code, timer} = installCodeCache[url];
|
'raw.githubusercontent.com': {
|
||||||
clearInstallCode(url);
|
glob: '/*',
|
||||||
clearTimeout(timer);
|
rx: /^(\/[^/]+?){4}\.user\.(css|styl)$/,
|
||||||
return code;
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Faster installation on known distribution sites to avoid flicker of css text
|
chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
|
||||||
chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => {
|
|
||||||
openInstallerPage(tabId, url, {});
|
|
||||||
// Silently suppressing navigation like it never happened
|
|
||||||
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
|
|
||||||
}, {
|
|
||||||
urls: [
|
urls: [
|
||||||
URLS.usoArchiveRaw + 'usercss/*.user.css',
|
URLS.usw + 'api/style/*.user.css',
|
||||||
'*://greasyfork.org/scripts/*/code/*.user.css',
|
...URLS.usoArchiveRaw.map(s => s + 'usercss/*.user.css'),
|
||||||
'*://sleazyfork.org/scripts/*/code/*.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'],
|
types: ['main_frame'],
|
||||||
}, ['blocking']);
|
}, ['blocking']);
|
||||||
|
|
||||||
// Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow
|
chrome.webRequest.onHeadersReceived.addListener(rememberContentType, {
|
||||||
chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => {
|
urls: makeUsercssGlobs('*', '/*'),
|
||||||
const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
|
|
||||||
tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
|
|
||||||
}, {
|
|
||||||
urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','),
|
|
||||||
types: ['main_frame'],
|
types: ['main_frame'],
|
||||||
}, ['responseHeaders']);
|
}, ['responseHeaders']);
|
||||||
|
|
||||||
tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => {
|
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 makeUsercssGlobs(host, path) {
|
||||||
|
return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeInstall({tabId, url, oldUrl = ''}) {
|
||||||
if (url.includes('.user.') &&
|
if (url.includes('.user.') &&
|
||||||
/^(https?|file|ftps?):/.test(url) &&
|
/^(https?|file|ftps?):/.test(url) &&
|
||||||
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
|
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
|
||||||
!oldUrl.startsWith(URLS.installUsercss)) {
|
!oldUrl.startsWith(URLS.installUsercss)) {
|
||||||
const inTab = url.startsWith('file:') && Boolean(fileLoader);
|
const inTab = url.startsWith('file:') && !chrome.app;
|
||||||
const code = await (inTab ? fileLoader : urlLoader)(tabId, url);
|
const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
|
||||||
if (/==userstyle==/i.test(code)) {
|
if (!/^\s*</.test(code) && RX_META.test(code)) {
|
||||||
openInstallerPage(tabId, url, {code, inTab});
|
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 {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openInstallerPage(tabId, url, {code, inTab} = {}) {
|
function openInstallerPage(tabId, url, {code, inTab} = {}) {
|
||||||
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
|
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
|
||||||
|
|
@ -79,4 +120,10 @@
|
||||||
chrome.tabs.update(tabId, {url: newUrl});
|
chrome.tabs.update(tabId, {url: newUrl});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
/** 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
160
background/usercss-manager.js
Normal file
160
background/usercss-manager.js
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
/* 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,
|
||||||
|
}),
|
||||||
|
|
||||||
|
async assignVars(style, oldStyle) {
|
||||||
|
const meta = style.usercssData;
|
||||||
|
const vars = meta.vars;
|
||||||
|
const oldVars = (oldStyle.usercssData || {}).vars;
|
||||||
|
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 ? {usercssData: {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) {
|
||||||
|
return API.styles.install(await usercssMan.parse(style));
|
||||||
|
},
|
||||||
|
|
||||||
|
async parse(style) {
|
||||||
|
style = await usercssMan.buildMeta(style);
|
||||||
|
// preserve style.vars during update
|
||||||
|
const dup = await usercssMan.find(style);
|
||||||
|
if (dup) {
|
||||||
|
style.id = dup.id;
|
||||||
|
await usercssMan.assignVars(style, dup);
|
||||||
|
}
|
||||||
|
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, ' ');
|
||||||
|
}
|
||||||
119
background/usw-api.js
Normal file
119
background/usw-api.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
/* 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]) => `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v || ''}`),
|
||||||
|
'==/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 data = (style._usw || {}).token
|
||||||
|
? style._usw
|
||||||
|
: await linkStyle(style, sourceCode);
|
||||||
|
const header = style.usercssData ? '' : fakeUsercssHeader(style);
|
||||||
|
return uswFetch(`style/${data.id}`, data.token, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({code: header + sourceCode}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
243
content/apply.js
243
content/apply.js
|
|
@ -1,34 +1,28 @@
|
||||||
/* global msg API prefs createStyleInjector */
|
/* global API msg */// msg.js
|
||||||
|
/* global StyleInjector */
|
||||||
|
/* global prefs */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Chrome reruns content script when documentElement is replaced.
|
(() => {
|
||||||
// Note, we're checking against a literal `1`, not just `if (truthy)`,
|
if (window.INJECTED === 1) return;
|
||||||
// because <html id="INJECTED"> is exposed per HTML spec as a global variable and `window.INJECTED`.
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
/** true -> when the page styles are received,
|
||||||
self.INJECTED !== 1 && (() => {
|
* false -> when disableAll mode is on at start, the styles won't be sent
|
||||||
self.INJECTED = 1;
|
* 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 IS_TAB = !chrome.tabs || location.pathname !== '/popup.html';
|
let hasStyles = false;
|
||||||
const IS_FRAME = window !== parent;
|
let isDisabled = false;
|
||||||
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
|
let isTab = !chrome.tabs || location.pathname !== '/popup.html';
|
||||||
const styleInjector = createStyleInjector({
|
const isFrame = window !== parent;
|
||||||
|
const isFrameAboutBlank = isFrame && location.href === 'about:blank';
|
||||||
|
const isUnstylable = !chrome.app && document instanceof XMLDocument;
|
||||||
|
const styleInjector = StyleInjector({
|
||||||
compare: (a, b) => a.id - b.id,
|
compare: (a, b) => a.id - b.id,
|
||||||
onUpdate: onInjectorUpdate,
|
onUpdate: onInjectorUpdate,
|
||||||
});
|
});
|
||||||
const initializing = init();
|
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
|
||||||
/** @type chrome.runtime.Port */
|
let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
|
||||||
let port;
|
location.href;
|
||||||
let lazyBadge = IS_FRAME;
|
|
||||||
let parentDomain;
|
|
||||||
|
|
||||||
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
|
|
||||||
if (!IS_TAB) {
|
|
||||||
chrome.tabs.getCurrent(tab => {
|
|
||||||
IS_TAB = Boolean(tab);
|
|
||||||
if (tab && styleInjector.list.length) updateCount();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// save it now because chrome.runtime will be unavailable in the orphaned script
|
// save it now because chrome.runtime will be unavailable in the orphaned script
|
||||||
const orphanEventId = chrome.runtime.id;
|
const orphanEventId = chrome.runtime.id;
|
||||||
|
|
@ -36,6 +30,37 @@ self.INJECTED !== 1 && (() => {
|
||||||
// firefox doesn't orphanize content scripts so the old elements stay
|
// firefox doesn't orphanize content scripts so the old elements stay
|
||||||
if (!chrome.app) styleInjector.clearOrphans();
|
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 {IntersectionObserver} = window;
|
||||||
|
const xoEventId = `${Math.random()}`;
|
||||||
|
/** @type IntersectionObserver */
|
||||||
|
let xo;
|
||||||
|
if (IntersectionObserver) {
|
||||||
|
window[Symbol.for('xo')] = (el, cb) => {
|
||||||
|
if (!xo) xo = new IntersectionObserver(onIntersect, {rootMargin: '100%'});
|
||||||
|
el.addEventListener(xoEventId, cb, {once: true});
|
||||||
|
xo.observe(el);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare all vars before init() or it'll throw due to "temporal dead zone" of const/let
|
||||||
|
const ready = 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);
|
msg.onTab(applyOnMessage);
|
||||||
|
|
||||||
if (!chrome.tabs) {
|
if (!chrome.tabs) {
|
||||||
|
|
@ -54,116 +79,107 @@ self.INJECTED !== 1 && (() => {
|
||||||
if (!isOrphaned) {
|
if (!isOrphaned) {
|
||||||
updateCount();
|
updateCount();
|
||||||
const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
|
const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
|
||||||
onOff(['disableAll'], updateDisableAll);
|
onOff('disableAll', updateDisableAll);
|
||||||
if (IS_FRAME) {
|
if (isFrame) {
|
||||||
updateExposeIframes();
|
updateExposeIframes();
|
||||||
onOff(['exposeIframes'], updateExposeIframes);
|
onOff('exposeIframes', updateExposeIframes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (STYLE_VIA_API) {
|
if (isUnstylable) {
|
||||||
await API.styleViaAPI({method: 'styleApply'});
|
await API.styleViaAPI({method: 'styleApply'});
|
||||||
} else {
|
} else {
|
||||||
const styles = chrome.app && getStylesViaXhr() ||
|
const SYM_ID = 'styles';
|
||||||
await API.getSectionsByUrl(getMatchUrl(), null, true);
|
const SYM = Symbol.for(SYM_ID);
|
||||||
if (styles.disableAll) {
|
const parentStyles = isFrameAboutBlank &&
|
||||||
delete styles.disableAll;
|
tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
|
||||||
styleInjector.toggle(false);
|
const styles =
|
||||||
|
window[SYM] ||
|
||||||
|
parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
|
||||||
|
!isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
|
||||||
|
await API.styles.getSectionsByUrl(matchUrl, null, true);
|
||||||
|
isDisabled = styles.disableAll;
|
||||||
|
hasStyles = !isDisabled;
|
||||||
|
if (hasStyles) {
|
||||||
|
window[SYM] = styles;
|
||||||
|
await styleInjector.apply(styles);
|
||||||
|
} else {
|
||||||
|
delete window[SYM];
|
||||||
|
prefs.subscribe('disableAll', updateDisableAll);
|
||||||
}
|
}
|
||||||
await styleInjector.apply(styles);
|
styleInjector.toggle(hasStyles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Must be executed inside try/catch */
|
||||||
function getStylesViaXhr() {
|
function getStylesViaXhr() {
|
||||||
if (new RegExp(`(^|\\s|;)${chrome.runtime.id}=\\s*([-\\w]+)\\s*(;|$)`).test(document.cookie)) {
|
const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
|
||||||
const data = RegExp.$2;
|
const url = 'blob:' + chrome.runtime.getURL(blobId);
|
||||||
const disableAll = data[0] === '1';
|
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
|
||||||
const url = 'blob:' + chrome.runtime.getURL(data.slice(1));
|
const xhr = new XMLHttpRequest();
|
||||||
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
|
xhr.open('GET', url, false); // synchronous
|
||||||
let res;
|
xhr.send();
|
||||||
try {
|
URL.revokeObjectURL(url);
|
||||||
if (!disableAll) { // will get the styles asynchronously
|
return JSON.parse(xhr.response);
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('GET', url, false); // synchronous
|
|
||||||
xhr.send();
|
|
||||||
res = JSON.parse(xhr.response);
|
|
||||||
}
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (e) {}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatchUrl() {
|
|
||||||
let matchUrl = location.href;
|
|
||||||
if (!chrome.tabs && !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 (IS_FRAME) {
|
|
||||||
matchUrl = parent.location.href;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
return matchUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyOnMessage(request) {
|
function applyOnMessage(request) {
|
||||||
if (STYLE_VIA_API) {
|
const {method} = request;
|
||||||
if (request.method === 'urlChanged') {
|
if (isUnstylable) {
|
||||||
|
if (method === 'urlChanged') {
|
||||||
request.method = 'styleReplaceAll';
|
request.method = 'styleReplaceAll';
|
||||||
}
|
}
|
||||||
if (/^(style|updateCount)/.test(request.method)) {
|
if (/^(style|updateCount)/.test(method)) {
|
||||||
API.styleViaAPI(request);
|
API.styleViaAPI(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.method) {
|
const {style} = request;
|
||||||
|
switch (method) {
|
||||||
case 'ping':
|
case 'ping':
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
styleInjector.remove(request.style.id);
|
styleInjector.remove(style.id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (request.style.enabled) {
|
if (!hasStyles && isDisabled) break;
|
||||||
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
if (style.enabled) {
|
||||||
.then(sections => {
|
API.styles.getSectionsByUrl(matchUrl, style.id).then(sections =>
|
||||||
if (!sections[request.style.id]) {
|
sections[style.id]
|
||||||
styleInjector.remove(request.style.id);
|
? styleInjector.apply(sections)
|
||||||
} else {
|
: styleInjector.remove(style.id));
|
||||||
styleInjector.apply(sections);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
styleInjector.remove(request.style.id);
|
styleInjector.remove(style.id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
if (request.style.enabled) {
|
if (!hasStyles && isDisabled) break;
|
||||||
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
if (style.enabled) {
|
||||||
|
API.styles.getSectionsByUrl(matchUrl, style.id)
|
||||||
.then(styleInjector.apply);
|
.then(styleInjector.apply);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'urlChanged':
|
case 'urlChanged':
|
||||||
API.getSectionsByUrl(getMatchUrl())
|
if (!hasStyles && isDisabled || matchUrl === request.url) break;
|
||||||
.then(styleInjector.replace);
|
matchUrl = request.url;
|
||||||
|
API.styles.getSectionsByUrl(matchUrl).then(sections => {
|
||||||
|
hasStyles = true;
|
||||||
|
styleInjector.replace(sections);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'backgroundReady':
|
case 'backgroundReady':
|
||||||
initializing
|
ready.catch(err =>
|
||||||
.catch(err => {
|
msg.isIgnorableError(err)
|
||||||
if (msg.RX_NO_RECEIVER.test(err.message)) {
|
? init()
|
||||||
return init();
|
: console.error(err));
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'updateCount':
|
case 'updateCount':
|
||||||
|
|
@ -173,8 +189,11 @@ self.INJECTED !== 1 && (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDisableAll(key, disableAll) {
|
function updateDisableAll(key, disableAll) {
|
||||||
if (STYLE_VIA_API) {
|
isDisabled = disableAll;
|
||||||
|
if (isUnstylable) {
|
||||||
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
||||||
|
} else if (!hasStyles && !disableAll) {
|
||||||
|
init();
|
||||||
} else {
|
} else {
|
||||||
styleInjector.toggle(!disableAll);
|
styleInjector.toggle(!disableAll);
|
||||||
}
|
}
|
||||||
|
|
@ -196,8 +215,8 @@ self.INJECTED !== 1 && (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCount() {
|
function updateCount() {
|
||||||
if (!IS_TAB) return;
|
if (!isTab) return;
|
||||||
if (IS_FRAME) {
|
if (isFrame) {
|
||||||
if (!port && styleInjector.list.length) {
|
if (!port && styleInjector.list.length) {
|
||||||
port = chrome.runtime.connect({name: 'iframe'});
|
port = chrome.runtime.connect({name: 'iframe'});
|
||||||
} else if (port && !styleInjector.list.length) {
|
} else if (port && !styleInjector.list.length) {
|
||||||
|
|
@ -205,23 +224,43 @@ self.INJECTED !== 1 && (() => {
|
||||||
}
|
}
|
||||||
if (lazyBadge && performance.now() > 1000) lazyBadge = false;
|
if (lazyBadge && performance.now() > 1000) lazyBadge = false;
|
||||||
}
|
}
|
||||||
(STYLE_VIA_API ?
|
(isUnstylable ?
|
||||||
API.styleViaAPI({method: 'updateCount'}) :
|
API.styleViaAPI({method: 'updateCount'}) :
|
||||||
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
|
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
|
||||||
).catch(msg.ignoreError);
|
).catch(msg.ignoreError);
|
||||||
}
|
}
|
||||||
|
|
||||||
function orphanCheck() {
|
function onFrameElementInView(cb) {
|
||||||
|
if (IntersectionObserver) {
|
||||||
|
parent[parent.Symbol.for('xo')](frameElement, cb);
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryCatch(func, ...args) {
|
||||||
try {
|
try {
|
||||||
if (chrome.i18n.getUILanguage()) return;
|
return func(...args);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function orphanCheck() {
|
||||||
|
if (tryCatch(() => chrome.i18n.getUILanguage())) return;
|
||||||
// In Chrome content script is orphaned on an extension update/reload
|
// In Chrome content script is orphaned on an extension update/reload
|
||||||
// so we need to detach event listeners
|
// so we need to detach event listeners
|
||||||
window.removeEventListener(orphanEventId, orphanCheck, true);
|
window.removeEventListener(orphanEventId, orphanCheck, true);
|
||||||
isOrphaned = true;
|
isOrphaned = true;
|
||||||
styleInjector.clear();
|
setTimeout(styleInjector.clear, 1000); // avoiding FOUC
|
||||||
try {
|
tryCatch(msg.off, applyOnMessage);
|
||||||
msg.off(applyOnMessage);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* global API */
|
/* global API */// msg.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// onCommitted may fire twice
|
// onCommitted may fire twice
|
||||||
|
|
@ -13,7 +13,7 @@ if (window.INJECTED_GREASYFORK !== 1) {
|
||||||
e.data.name &&
|
e.data.name &&
|
||||||
e.data.type === 'style-version-query') {
|
e.data.type === 'style-version-query') {
|
||||||
removeEventListener('message', onMessage);
|
removeEventListener('message', onMessage);
|
||||||
const style = await API.findUsercss(e.data) || {};
|
const style = await API.usercss.find(e.data) || {};
|
||||||
const {version} = style.usercssData || {};
|
const {version} = style.usercssData || {};
|
||||||
postMessage({type: 'style-version', version}, '*');
|
postMessage({type: 'style-version', version}, '*');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
/* 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,19 +1,21 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
|
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
|
||||||
if (typeof self.oldCode !== 'string') {
|
if (typeof window.oldCode !== 'string') {
|
||||||
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
|
window.oldCode = (document.querySelector('body > pre') || document.body).textContent;
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
chrome.runtime.onConnect.addListener(port => {
|
||||||
if (port.name !== 'downloadSelf') return;
|
if (port.name !== 'downloadSelf') return;
|
||||||
port.onMessage.addListener(({id, force}) => {
|
port.onMessage.addListener(async ({id, force}) => {
|
||||||
fetch(location.href, {mode: 'same-origin'})
|
const msg = {id};
|
||||||
.then(r => r.text())
|
try {
|
||||||
.then(code => ({id, code: force || code !== self.oldCode ? code : null}))
|
const code = await (await fetch(location.href, {mode: 'same-origin'})).text();
|
||||||
.catch(error => ({id, error: error.message || `${error}`}))
|
if (code !== window.oldCode || force) {
|
||||||
.then(msg => {
|
msg.code = window.oldCode = code;
|
||||||
port.postMessage(msg);
|
}
|
||||||
if (msg.code != null) self.oldCode = msg.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
|
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
|
||||||
addEventListener('pagehide', () => port.disconnect(), {once: true});
|
addEventListener('pagehide', () => port.disconnect(), {once: true});
|
||||||
|
|
@ -21,4 +23,4 @@ if (typeof self.oldCode !== 'string') {
|
||||||
}
|
}
|
||||||
|
|
||||||
// passing the result to tabs.executeScript
|
// passing the result to tabs.executeScript
|
||||||
self.oldCode; // eslint-disable-line no-unused-expressions
|
window.oldCode; // eslint-disable-line no-unused-expressions
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* global cloneInto msg API */
|
/* global API msg */// msg.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
|
@ -14,17 +14,10 @@
|
||||||
|
|
||||||
msg.on(onMessage);
|
msg.on(onMessage);
|
||||||
|
|
||||||
onDOMready().then(() => {
|
|
||||||
window.postMessage({
|
|
||||||
direction: 'from-content-script',
|
|
||||||
message: 'StylishInstalled',
|
|
||||||
}, '*');
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentMd5;
|
let currentMd5;
|
||||||
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
|
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
API.findStyle({md5Url}),
|
API.styles.find({md5Url}),
|
||||||
getResource(md5Url),
|
getResource(md5Url),
|
||||||
onDOMready(),
|
onDOMready(),
|
||||||
]).then(checkUpdatability);
|
]).then(checkUpdatability);
|
||||||
|
|
@ -85,7 +78,7 @@
|
||||||
const observer = new MutationObserver(check);
|
const observer = new MutationObserver(check);
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true
|
subtree: true,
|
||||||
});
|
});
|
||||||
check();
|
check();
|
||||||
|
|
||||||
|
|
@ -105,7 +98,7 @@
|
||||||
? 'styleCanBeUpdatedChrome'
|
? 'styleCanBeUpdatedChrome'
|
||||||
: 'styleAlreadyInstalledChrome',
|
: 'styleAlreadyInstalledChrome',
|
||||||
detail: {
|
detail: {
|
||||||
updateUrl: installedStyle.updateUrl
|
updateUrl: installedStyle.updateUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -119,13 +112,11 @@
|
||||||
if (typeof cloneInto !== 'undefined') {
|
if (typeof cloneInto !== 'undefined') {
|
||||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||||
detail = cloneInto({detail}, document);
|
detail = cloneInto({detail}, document); /* global cloneInto */
|
||||||
} else {
|
} else {
|
||||||
detail = {detail};
|
detail = {detail};
|
||||||
}
|
}
|
||||||
onDOMready().then(() => {
|
document.dispatchEvent(new CustomEvent(type, detail));
|
||||||
document.dispatchEvent(new CustomEvent(type, detail));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(event) {
|
function onClick(event) {
|
||||||
|
|
@ -154,9 +145,9 @@
|
||||||
|
|
||||||
function doInstall() {
|
function doInstall() {
|
||||||
let oldStyle;
|
let oldStyle;
|
||||||
return API.findStyle({
|
return API.styles.find({
|
||||||
md5Url: getMeta('stylish-md5-url') || location.href
|
md5Url: getMeta('stylish-md5-url') || location.href,
|
||||||
}, true)
|
})
|
||||||
.then(_oldStyle => {
|
.then(_oldStyle => {
|
||||||
oldStyle = _oldStyle;
|
oldStyle = _oldStyle;
|
||||||
return oldStyle ?
|
return oldStyle ?
|
||||||
|
|
@ -172,7 +163,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveStyleCode(message, name, addProps = {}) {
|
async function saveStyleCode(message, name, addProps = {}) {
|
||||||
const isNew = message === 'styleInstall';
|
const isNew = message === 'styleInstall';
|
||||||
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
||||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||||
|
|
@ -180,22 +171,19 @@
|
||||||
}
|
}
|
||||||
saveStyleCode.confirmed = true;
|
saveStyleCode.confirmed = true;
|
||||||
enableUpdateButton(false);
|
enableUpdateButton(false);
|
||||||
return getStyleJson().then(json => {
|
const json = await getStyleJson();
|
||||||
if (!json) {
|
if (!json) {
|
||||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||||
'https://github.com/openstyles/stylus/issues/195');
|
'https://github.com/openstyles/stylus/issues/195');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
|
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
|
||||||
return API.installStyle(Object.assign(json, addProps, {originalMd5: currentMd5}))
|
const style = await API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}));
|
||||||
.then(style => {
|
if (!isNew && style.updateUrl.includes('?')) {
|
||||||
if (!isNew && style.updateUrl.includes('?')) {
|
enableUpdateButton(true);
|
||||||
enableUpdateButton(true);
|
} else {
|
||||||
} else {
|
sendEvent({type: 'styleInstalledChrome'});
|
||||||
sendEvent({type: 'styleInstalledChrome'});
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function enableUpdateButton(state) {
|
function enableUpdateButton(state) {
|
||||||
const important = s => s.replace(/;/g, '!important;');
|
const important = s => s.replace(/;/g, '!important;');
|
||||||
|
|
@ -218,86 +206,59 @@
|
||||||
return e ? e.getAttribute('href') : null;
|
return e ? e.getAttribute('href') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResource(url, options) {
|
async function getResource(url, opts) {
|
||||||
if (url.startsWith('#')) {
|
try {
|
||||||
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
|
return url.startsWith('#')
|
||||||
|
? document.getElementById(url.slice(1)).textContent
|
||||||
|
: await API.download(url, opts);
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error\n' + error.message);
|
||||||
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
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"
|
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
||||||
// instead of "https://update.userstyles.org/#####.md5"
|
// instead of "https://update.userstyles.org/#####.md5"
|
||||||
function tryFixMd5(style) {
|
async function getStyleJson() {
|
||||||
if (style && style.md5Url && style.md5Url.includes('update.update')) {
|
try {
|
||||||
style.md5Url = style.md5Url.replace('update.update', 'update');
|
const style = await getResource(getStyleURL(), {responseType: 'json'});
|
||||||
}
|
const codeElement = document.getElementById('stylish-code');
|
||||||
return style;
|
if (!style || !Array.isArray(style.sections) || style.sections.length ||
|
||||||
}
|
codeElement && !codeElement.textContent.trim()) {
|
||||||
|
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);
|
const code = await getResource(getMeta('stylish-update-url'));
|
||||||
}
|
style.sections = (await API.worker.parseMozFormat({code})).sections;
|
||||||
|
if (style.md5Url) style.md5Url = style.md5Url.replace('update.update', 'update');
|
||||||
|
return style;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
function equalOrEmpty(a, b, telltale, comparator) {
|
/**
|
||||||
const typeA = a && typeof a[telltale] === 'function';
|
* The sections are checked in successive order because it matters when many sections
|
||||||
const typeB = b && typeof b[telltale] === 'function';
|
* match the same URL and they have rules with the same CSS specificity
|
||||||
return (
|
* @param {Object} a - first style object
|
||||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
* @param {Object} b - second style object
|
||||||
(b === null || b === undefined || (typeB && !b.length))
|
* @returns {?boolean}
|
||||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
*/
|
||||||
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
|
const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
|
||||||
|
return a && b && a.length === b.length && a.every(sameSection);
|
||||||
|
function sameSection(secA, i) {
|
||||||
|
return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
|
||||||
|
targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
|
||||||
}
|
}
|
||||||
|
function equalOrEmpty(a, b, type, comparator) {
|
||||||
function arrayMirrors(array1, array2) {
|
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
|
||||||
return (
|
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
|
||||||
array1.every(el => array2.includes(el)) &&
|
return typeA && typeB && comparator(a, b) ||
|
||||||
array2.every(el => array1.includes(el))
|
(a == null || typeA && !a.length) &&
|
||||||
);
|
(b == null || typeB && !b.length);
|
||||||
|
}
|
||||||
|
function arrayMirrors(a, b) {
|
||||||
|
return a.length === b.length &&
|
||||||
|
a.every(el => b.includes(el)) &&
|
||||||
|
b.every(el => a.includes(el));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,6 +304,7 @@
|
||||||
|
|
||||||
function inPageContext(eventId) {
|
function inPageContext(eventId) {
|
||||||
document.currentScript.remove();
|
document.currentScript.remove();
|
||||||
|
window.isInstalled = true;
|
||||||
const origMethods = {
|
const origMethods = {
|
||||||
json: Response.prototype.json,
|
json: Response.prototype.json,
|
||||||
byId: document.getElementById,
|
byId: document.getElementById,
|
||||||
|
|
@ -365,13 +327,33 @@ function inPageContext(eventId) {
|
||||||
Response.prototype.json = origMethods.json;
|
Response.prototype.json = origMethods.json;
|
||||||
const images = new Map();
|
const images = new Map();
|
||||||
for (const ss of json.style_settings) {
|
for (const ss of json.style_settings) {
|
||||||
const value = vars.get('ik-' + ss.install_key);
|
let value = vars.get('ik-' + ss.install_key);
|
||||||
if (value && ss.setting_type === 'image' && ss.style_setting_options) {
|
if (!value || !(ss.style_setting_options || [])[0]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value.startsWith('ik-')) {
|
||||||
|
value = value.replace(/^ik-/, '');
|
||||||
|
const def = ss.style_setting_options.find(item => item.default);
|
||||||
|
if (!def || def.install_key !== value) {
|
||||||
|
if (def) def.default = false;
|
||||||
|
for (const item of ss.style_setting_options) {
|
||||||
|
if (item.install_key === value) {
|
||||||
|
item.default = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ss.setting_type === 'image') {
|
||||||
let isListed;
|
let isListed;
|
||||||
for (const opt of ss.style_setting_options) {
|
for (const opt of ss.style_setting_options) {
|
||||||
isListed |= opt.default = (opt.value === value);
|
isListed |= opt.default = (opt.value === value);
|
||||||
}
|
}
|
||||||
images.set(ss.install_key, {url: value, isListed});
|
images.set(ss.install_key, {url: value, isListed});
|
||||||
|
} else {
|
||||||
|
const item = ss.style_setting_options[0];
|
||||||
|
if (item.value !== value && item.install_key === 'placeholder') {
|
||||||
|
item.value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (images.size) {
|
if (images.size) {
|
||||||
|
|
|
||||||
43
content/install-hook-userstylesworld.js
Normal file
43
content/install-hook-userstylesworld.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* 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,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
/** @type {function(opts):StyleInjector} */
|
||||||
|
window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
|
||||||
compare,
|
compare,
|
||||||
onUpdate = () => {},
|
onUpdate = () => {},
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -8,8 +9,6 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
||||||
const PATCH_ID = 'transition-patch';
|
const PATCH_ID = 'transition-patch';
|
||||||
// styles are out of order if any of these elements is injected between them
|
// 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 ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
|
||||||
// detect Chrome 65 via a feature it added since browser version can be spoofed
|
|
||||||
const isChromePre65 = chrome.app && typeof Worklet !== 'function';
|
|
||||||
const docRewriteObserver = RewriteObserver(_sort);
|
const docRewriteObserver = RewriteObserver(_sort);
|
||||||
const docRootObserver = RootObserver(_sortIfNeeded);
|
const docRootObserver = RootObserver(_sortIfNeeded);
|
||||||
const list = [];
|
const list = [];
|
||||||
|
|
@ -19,22 +18,22 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
||||||
// will store the original method refs because the page can override them
|
// will store the original method refs because the page can override them
|
||||||
let creationDoc, createElement, createElementNS;
|
let creationDoc, createElement, createElementNS;
|
||||||
|
|
||||||
return {
|
return /** @namespace StyleInjector */ {
|
||||||
|
|
||||||
list,
|
list,
|
||||||
|
|
||||||
apply(styleMap) {
|
async apply(styleMap) {
|
||||||
const styles = _styleMapToArray(styleMap);
|
const styles = _styleMapToArray(styleMap);
|
||||||
return (
|
const value = !styles.length
|
||||||
!styles.length ?
|
? []
|
||||||
Promise.resolve([]) :
|
: await docRootObserver.evade(() => {
|
||||||
docRootObserver.evade(() => {
|
if (!isTransitionPatched && isEnabled) {
|
||||||
if (!isTransitionPatched && isEnabled) {
|
_applyTransitionPatch(styles);
|
||||||
_applyTransitionPatch(styles);
|
}
|
||||||
}
|
return styles.map(_addUpdate);
|
||||||
return styles.map(_addUpdate);
|
});
|
||||||
})
|
_emitUpdate();
|
||||||
).then(_emitUpdate);
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
|
@ -157,10 +156,9 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
||||||
docRootObserver[onOff]();
|
docRootObserver[onOff]();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _emitUpdate(value) {
|
function _emitUpdate() {
|
||||||
_toggleObservers(list.length);
|
_toggleObservers(list.length);
|
||||||
onUpdate();
|
onUpdate();
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -232,17 +230,8 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
||||||
|
|
||||||
function _update({id, code}) {
|
function _update({id, code}) {
|
||||||
const style = table.get(id);
|
const style = table.get(id);
|
||||||
if (style.code === code) return;
|
if (style.code !== code) {
|
||||||
style.code = code;
|
style.code = code;
|
||||||
// workaround for Chrome devtools bug fixed in v65
|
|
||||||
if (isChromePre65) {
|
|
||||||
const oldEl = style.el;
|
|
||||||
style.el = _createStyle(id, code);
|
|
||||||
if (isEnabled) {
|
|
||||||
oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling);
|
|
||||||
oldEl.remove();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
style.el.textContent = code;
|
style.el.textContent = code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
394
edit.html
394
edit.html
|
|
@ -1,112 +1,67 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<html id="stylus">
|
<html id="stylus">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<link href="global.css" rel="stylesheet">
|
<link href="global.css" rel="stylesheet">
|
||||||
<link href="edit/edit.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
|
||||||
|
|
||||||
<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 id="cm-theme" rel="stylesheet">
|
<link id="cm-theme" rel="stylesheet">
|
||||||
|
|
||||||
<script src="js/polyfill.js"></script>
|
<script src="js/polyfill.js"></script>
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/toolbox.js"></script>
|
||||||
<script src="js/messaging.js"></script>
|
|
||||||
<script src="js/msg.js"></script>
|
<script src="js/msg.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
|
<script src="js/dom.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
<script src="js/script-loader.js"></script>
|
|
||||||
<script src="js/storage-util.js"></script>
|
|
||||||
|
|
||||||
<script src="content/style-injector.js"></script>
|
<script src="content/style-injector.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
|
|
||||||
<script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style -->
|
<script src="js/sections-util.js"></script>
|
||||||
|
<script src="edit/codemirror-themes.js"></script> <!-- must precede base.js -->
|
||||||
|
<script src="edit/base.js"></script>
|
||||||
|
|
||||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
|
||||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/mode/css/css.js"></script>
|
<script src="vendor/codemirror/mode/css/css.js"></script>
|
||||||
|
<script src="vendor/codemirror/mode/stylus/stylus.js"></script>
|
||||||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
|
||||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/addon/edit/closebrackets.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/scroll/annotatescrollbar.js"></script>
|
||||||
<script src="vendor/codemirror/addon/search/searchcursor.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/comment/comment.js"></script>
|
||||||
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
||||||
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
<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/foldcode.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/indent-fold.js"></script>
|
<script src="vendor/codemirror/addon/fold/indent-fold.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/comment-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>
|
<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/show-hint.js"></script>
|
||||||
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
||||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
|
||||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
|
||||||
|
|
||||||
<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="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
|
||||||
|
<script src="vendor/lz-string-unsafe/lz-string-unsafe.min.js"></script>
|
||||||
|
|
||||||
<script src="msgbox/msgbox.js" async></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/storage-util.js"></script>
|
||||||
|
<script src="js/worker-util.js"></script>
|
||||||
|
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
<script src="edit/util.js"></script>
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
<script src="edit/codemirror-factory.js"></script>
|
<script src="edit/codemirror-factory.js"></script>
|
||||||
<script src="edit/util.js"></script>
|
<script src="edit/moz-section-finder.js"></script>
|
||||||
<script src="edit/regexp-tester.js"></script>
|
<script src="edit/moz-section-widget.js"></script>
|
||||||
<script src="edit/live-preview.js"></script>
|
<script src="edit/linter-manager.js"></script>
|
||||||
<script src="edit/applies-to-line-widget.js"></script>
|
|
||||||
<script src="edit/reroute-hotkeys.js"></script>
|
|
||||||
<link href="edit/global-search.css" rel="stylesheet">
|
|
||||||
<script src="edit/global-search.js"></script>
|
|
||||||
<script src="edit/colorpicker-helper.js"></script>
|
|
||||||
<script src="edit/beautify.js"></script>
|
<script src="edit/beautify.js"></script>
|
||||||
<script src="edit/show-keymap-help.js"></script>
|
|
||||||
<script src="edit/codemirror-themes.js"></script>
|
|
||||||
<script src="edit/source-editor.js"></script>
|
<script src="edit/source-editor.js"></script>
|
||||||
<script src="edit/sections-editor-section.js"></script>
|
<script src="edit/sections-editor-section.js"></script>
|
||||||
<script src="edit/sections-editor.js"></script>
|
<script src="edit/sections-editor.js"></script>
|
||||||
|
<script src="edit/usw-integration.js"></script>
|
||||||
<script src="js/worker-util.js"></script>
|
<script src="edit/edit.js"></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>
|
|
||||||
|
|
||||||
<template data-id="appliesTo">
|
<template data-id="appliesTo">
|
||||||
<li class="applies-to-item">
|
<li class="applies-to-item">
|
||||||
|
|
@ -121,10 +76,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="applies-value-wrapper">
|
<div class="applies-value-wrapper">
|
||||||
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
|
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
|
||||||
<a class="remove-applies-to" href="#" i18n-text="appliesRemove" i18n-title="appliesRemove">
|
<a class="remove-applies-to" i18n-text="appliesRemove" i18n-title="appliesRemove" tabindex="0">
|
||||||
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
|
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="add-applies-to" href="#" i18n-text="appliesAdd" i18n-title="appliesAdd">
|
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" tabindex="0">
|
||||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,7 +88,7 @@
|
||||||
|
|
||||||
<template data-id="appliesToEverything">
|
<template data-id="appliesToEverything">
|
||||||
<li class="applies-to-everything" i18n-text="appliesToEverything">
|
<li class="applies-to-everything" i18n-text="appliesToEverything">
|
||||||
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" href="#">
|
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" tabindex="0">
|
||||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -148,7 +103,7 @@
|
||||||
<label i18n-text="sectionCode" class="code-label"></label>
|
<label i18n-text="sectionCode" class="code-label"></label>
|
||||||
<div class="applies-to">
|
<div class="applies-to">
|
||||||
<label i18n-text="appliesLabel">
|
<label i18n-text="appliesLabel">
|
||||||
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
|
<a class="svg-inline-wrapper applies-to-help" tabindex="0">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -171,14 +126,14 @@
|
||||||
<div data-type="main">
|
<div data-type="main">
|
||||||
<div data-type="content"></div>
|
<div data-type="content"></div>
|
||||||
<div data-type="actions">
|
<div data-type="actions">
|
||||||
<a data-action="case" i18n-title="searchCaseSensitive" href="#" tabindex="0">Aa</a>
|
<a data-action="case" i18n-title="searchCaseSensitive" tabindex="0">Aa</a>
|
||||||
<a data-action="prev" i18n-title="genericPrevious" href="#" data-hotkey-tooltip="findPrev" tabindex="0">
|
<a data-action="prev" i18n-title="genericPrevious" data-hotkey-tooltip="findPrev" tabindex="0">
|
||||||
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
|
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a data-action="next" i18n-title="genericNext" href="#" data-hotkey-tooltip="findNext" tabindex="0">
|
<a data-action="next" i18n-title="genericNext" data-hotkey-tooltip="findNext" tabindex="0">
|
||||||
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
|
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a data-action="close" i18n-title="confirmClose" href="#" data-hotkey-tooltip="=Esc" tabindex="0">
|
<a data-action="close" i18n-title="confirmClose" data-hotkey-tooltip="=Esc" tabindex="0">
|
||||||
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -274,15 +229,25 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<link href="edit/edit.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="stylus-edit">
|
<body id="stylus-edit">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
<h1 id="heading" i18n-data-edit="editStyleHeading" i18n-data-add="addStyleTitle"></h1>
|
||||||
<section id="basic-info">
|
<section id="basic-info">
|
||||||
<div id="basic-info-name">
|
<div id="basic-info-name">
|
||||||
<input id="name" class="style-contributor" spellcheck="false">
|
<input id="name" class="style-contributor" spellcheck="false">
|
||||||
<a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden>
|
<a id="reset-name" i18n-title="customNameResetHint" tabindex="0" hidden>
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
<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
|
<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 "/>
|
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||||
|
|
@ -298,7 +263,7 @@
|
||||||
<input type="checkbox" id="enabled" class="style-contributor">
|
<input type="checkbox" id="enabled" class="style-contributor">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
</label>
|
</label>
|
||||||
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
|
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip">
|
||||||
<input type="checkbox" id="editor.livePreview">
|
<input type="checkbox" id="editor.livePreview">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -311,154 +276,164 @@
|
||||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||||
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="mozilla-format-container">
|
<div id="mozilla-format-buttons" class="sectioned-only">
|
||||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading">
|
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||||
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0">
|
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0"
|
||||||
</a>
|
i18n-title="styleMozillaFormatHeading">
|
||||||
</h2>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
<div id="mozilla-format-buttons">
|
</a>
|
||||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
|
||||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<details id="options" data-pref="editor.options.expanded">
|
<div id="details-wrapper">
|
||||||
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
|
||||||
<div id="options-wrapper">
|
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
||||||
<div class="options-column">
|
<div id="options-wrapper">
|
||||||
<div class="option">
|
<div class="options-column">
|
||||||
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
|
<div class="option">
|
||||||
<input id="editor.lineWrapping" type="checkbox">
|
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<input id="editor.lineWrapping" type="checkbox">
|
||||||
</label>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
</div>
|
</label>
|
||||||
<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>
|
||||||
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
<div class="option">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<label id="smartIndent-label" i18n-text="cm_smartIndent">
|
||||||
</a>
|
<input id="editor.smartIndent" type="checkbox">
|
||||||
</div>
|
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||||
<div class="option aligned">
|
</label>
|
||||||
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
</div>
|
||||||
<div class="select-resizer">
|
<div class="option">
|
||||||
<select id="editor.theme"></select>
|
<label id="indentWithTabs-label" i18n-text="cm_indentWithTabs">
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
<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" 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-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>
|
</div>
|
||||||
<div class="option aligned">
|
<div class="options-column">
|
||||||
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
<div class="option aligned">
|
||||||
<div class="select-resizer">
|
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||||
<select id="editor.matchHighlight">
|
<input id="editor.tabSize" type="number" min="0">
|
||||||
<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>
|
||||||
</div>
|
<div class="option aligned">
|
||||||
<div class="option aligned">
|
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||||
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
|
|
||||||
<div class="select-resizer">
|
<div class="select-resizer">
|
||||||
<select id="editor.linter">
|
<select id="editor.keyMap"></select>
|
||||||
<option value="csslint" selected>CSSLint</option>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
<option value="stylelint">Stylelint</option>
|
</div>
|
||||||
<option value="" i18n-text="genericDisabledLabel"></option>
|
<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-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="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>
|
</select>
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<a id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
|
</div>
|
||||||
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
|
<div class="option aligned">
|
||||||
</a>
|
<label id="linter-label" for="editor.linter" i18n-text="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-text="genericDisabledLabel"></option>
|
||||||
|
</select>
|
||||||
|
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||||
|
</div>
|
||||||
|
<a id="linter-settings" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
|
||||||
|
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
</details>
|
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
|
||||||
<details id="lint" class="hidden-unless-compact" data-pref="editor.lint.expanded">
|
<summary><h2 i18n-text="publish"></h2></summary>
|
||||||
<summary>
|
<div>
|
||||||
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
|
<a id="usw-url" href="https://userstyles.world" target="_blank"> </a>
|
||||||
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0">
|
<div id="usw-link-info">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<dl><dt i18n-text="styleName"></dt><dd data-usw="name"></dd></dl>
|
||||||
</a>
|
<dl><dt i18n-text="genericDescription"></dt><dd data-usw="description"></dd></dl>
|
||||||
</h2>
|
</div>
|
||||||
</summary>
|
<div>
|
||||||
<div class="lint-scroll-container">
|
<button id="usw-publish-style"
|
||||||
<div class="lint-report-container"></div>
|
i18n-data-publish="publishStyle"
|
||||||
</div>
|
i18n-data-push="publishPush"></button>
|
||||||
</details>
|
<button id="usw-disconnect" i18n-text="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-text="sections"></h2></summary>
|
||||||
|
<ol id="toc"></ol>
|
||||||
|
</details>
|
||||||
|
<details id="lint" data-pref="editor.lint.expanded" class="hidden-unless-compact ignore-pref-if-compact">
|
||||||
|
<summary>
|
||||||
|
<h2 i18n-text="linterIssues">: <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>
|
||||||
|
</h2>
|
||||||
|
</summary>
|
||||||
|
<div class="lint-scroll-container">
|
||||||
|
<div class="lint-report-container"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<div id="footer" class="hidden">
|
<div id="footer" class="hidden">
|
||||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||||
i18n-text="externalUsercssDocument"
|
i18n-text="externalUsercssDocument"
|
||||||
target="_blank"></a>
|
target="_blank"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section id="sections">
|
<section id="sections"></section>
|
||||||
<!--
|
|
||||||
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 id="help-popup">
|
||||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||||
<div class="contents"></div>
|
<div class="contents"></div>
|
||||||
|
|
@ -482,8 +457,8 @@
|
||||||
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
|
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
|
<symbol id="svg-icon-config" 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"/>
|
<path d="M13.3,12.8l1.5-2.6l-2.2-1.5c0-0.2,0.1-0.5,0.1-0.7c0-0.2,0-0.5-0.1-0.7l2.2-1.5l-1.5-2.6l-2.4,1.2 c-0.4-0.3-0.8-0.5-1.2-0.7L9.5,1h-3L6.3,3.7C5.9,3.8,5.5,4.1,5.1,4.4L2.7,3.2L1.2,5.8l2.2,1.5c0,0.2-0.1,0.5-0.1,0.7 c0,0.2,0,0.5,0.1,0.7l-2.2,1.5l1.5,2.6l2.4-1.2c0.4,0.3,0.8,0.5,1.2,0.7L6.5,15h3l0.2-2.7c0.4-0.2,0.8-0.4,1.2-0.7L13.3,12.8z M8,10.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,1-2.3,2.3-2.3c1.3,0,2.3,1,2.3,2.3C10.3,9.3,9.3,10.3,8,10.3z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
||||||
|
|
@ -503,6 +478,5 @@
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,590 +0,0 @@
|
||||||
/* 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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
269
edit/autocomplete.js
Normal file
269
edit/autocomplete.js
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
/* 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)\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 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',
|
||||||
|
];
|
||||||
|
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 (!cssValues) cssValues = await linterMan.worker.getCssPropsValues();
|
||||||
|
list = [...new Set([...cssValues.own[prop] || [], ...cssValues.global])];
|
||||||
|
end = prev + execAt(/(\s*[-a-z(]+)?/y, prev, text)[0].length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// properties and media features
|
||||||
|
if (!list &&
|
||||||
|
/^(prop(erty|\?)|atom|error)/.test(type) &&
|
||||||
|
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
|
||||||
|
if (!cssProps) 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},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCssProps() {
|
||||||
|
cssProps = addSuffix(cssMime.propertyKeywords);
|
||||||
|
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) {
|
||||||
|
// simplified regex without CSS escapes
|
||||||
|
const rx = new RegExp(
|
||||||
|
'(?:^|[\\s/;{])(' +
|
||||||
|
(leftPart.startsWith('--') ? leftPart : '--') +
|
||||||
|
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
|
||||||
|
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
})();
|
||||||
418
edit/base.js
Normal file
418
edit/base.js
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
/* global $ $$ $create setupLivePrefs waitForSelector */// dom.js
|
||||||
|
/* global API */// msg.js
|
||||||
|
/* global CODEMIRROR_THEMES */
|
||||||
|
/* global CodeMirror */
|
||||||
|
/* global MozDocMapper */// sections-util.js
|
||||||
|
/* global initBeautifyButton */// beautify.js
|
||||||
|
/* global prefs */
|
||||||
|
/* global t */// localization.js
|
||||||
|
/* global
|
||||||
|
FIREFOX
|
||||||
|
debounce
|
||||||
|
getOwnTab
|
||||||
|
sessionStore
|
||||||
|
tryJSONparse
|
||||||
|
tryURL
|
||||||
|
*/// toolbox.js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type Editor
|
||||||
|
* @namespace Editor
|
||||||
|
*/
|
||||||
|
const editor = {
|
||||||
|
style: null,
|
||||||
|
dirty: DirtyReporter(),
|
||||||
|
isUsercss: false,
|
||||||
|
isWindowed: false,
|
||||||
|
lazyKeymaps: {
|
||||||
|
emacs: '/vendor/codemirror/keymap/emacs',
|
||||||
|
vim: '/vendor/codemirror/keymap/vim',
|
||||||
|
},
|
||||||
|
livePreview: null,
|
||||||
|
/** @type {'customName'|'name'} */
|
||||||
|
nameTarget: 'name',
|
||||||
|
previewDelay: 200, // Chrome devtools uses 200
|
||||||
|
scrollInfo: null,
|
||||||
|
|
||||||
|
onStyleUpdated() {
|
||||||
|
document.documentElement.classList.toggle('is-new-style', !editor.style.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
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 baseInit = (() => {
|
||||||
|
const domReady = waitForSelector('#sections');
|
||||||
|
|
||||||
|
return {
|
||||||
|
domReady,
|
||||||
|
ready: Promise.all([
|
||||||
|
domReady,
|
||||||
|
loadStyle(),
|
||||||
|
prefs.ready.then(() =>
|
||||||
|
Promise.all([
|
||||||
|
loadTheme(),
|
||||||
|
loadKeymaps(),
|
||||||
|
])),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Preloads vim/emacs keymap only if it's the active one, otherwise will load later */
|
||||||
|
function loadKeymaps() {
|
||||||
|
const km = prefs.get('editor.keyMap');
|
||||||
|
return /emacs/i.test(km) && require([editor.lazyKeymaps.emacs]) ||
|
||||||
|
/vim/i.test(km) && require([editor.lazyKeymaps.vim]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStyle() {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const id = Number(params.get('id'));
|
||||||
|
const style = id && await API.styles.get(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
|
||||||
|
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
|
||||||
|
editor.style = style;
|
||||||
|
editor.onStyleUpdated();
|
||||||
|
editor.updateTitle(false);
|
||||||
|
document.documentElement.classList.toggle('usercss', editor.isUsercss);
|
||||||
|
sessionStore.justEditedStyleId = style.id || '';
|
||||||
|
// no such style so let's clear the invalid URL parameters
|
||||||
|
if (!style.id) history.replaceState({}, '', location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preloads the theme so CodeMirror can use the correct metrics in its first render */
|
||||||
|
async function loadTheme() {
|
||||||
|
const theme = prefs.get('editor.theme');
|
||||||
|
if (!CODEMIRROR_THEMES.includes(theme)) {
|
||||||
|
prefs.set('editor.theme', 'default');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (theme !== 'default') {
|
||||||
|
const el = $('#cm-theme');
|
||||||
|
const el2 = await require([`/vendor/codemirror/theme/${theme}.css`]);
|
||||||
|
el2.id = el.id;
|
||||||
|
el.remove();
|
||||||
|
// FF containers take more time to load CSS
|
||||||
|
for (let retry = 0; !el2.sheet && ++retry <= 10;) {
|
||||||
|
await new Promise(requestAnimationFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region init layout/resize
|
||||||
|
|
||||||
|
baseInit.domReady.then(() => {
|
||||||
|
let headerHeight;
|
||||||
|
detectLayout(true);
|
||||||
|
window.on('resize', () => detectLayout());
|
||||||
|
|
||||||
|
function detectLayout(now) {
|
||||||
|
const compact = window.innerWidth <= 850;
|
||||||
|
if (compact) {
|
||||||
|
document.body.classList.add('compact-layout');
|
||||||
|
if (!editor.isUsercss) {
|
||||||
|
if (now) fixedHeader();
|
||||||
|
else debounce(fixedHeader, 250);
|
||||||
|
window.on('scroll', fixedHeader, {passive: true});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('compact-layout', 'fixed-header');
|
||||||
|
window.off('scroll', fixedHeader);
|
||||||
|
}
|
||||||
|
for (const el of $$('details[data-pref]')) {
|
||||||
|
el.open = compact ? false : prefs.get(el.dataset.pref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixedHeader() {
|
||||||
|
const headerFixed = $('.fixed-header');
|
||||||
|
if (!headerFixed) headerHeight = $('#header').clientHeight;
|
||||||
|
const scrollPoint = headerHeight - 43;
|
||||||
|
if (window.scrollY >= scrollPoint && !headerFixed) {
|
||||||
|
$('body').style.setProperty('--fixed-padding', ` ${headerHeight}px`);
|
||||||
|
$('body').classList.add('fixed-header');
|
||||||
|
} else if (window.scrollY < scrollPoint && headerFixed) {
|
||||||
|
$('body').classList.remove('fixed-header');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region init header
|
||||||
|
|
||||||
|
baseInit.ready.then(() => {
|
||||||
|
initBeautifyButton($('#beautify'));
|
||||||
|
initKeymapElement();
|
||||||
|
initNameArea();
|
||||||
|
initThemeElement();
|
||||||
|
setupLivePrefs();
|
||||||
|
|
||||||
|
require(Object.values(editor.lazyKeymaps), () => {
|
||||||
|
initKeymapElement();
|
||||||
|
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
|
||||||
|
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = false;
|
||||||
|
});
|
||||||
|
resetEl.hidden = !editor.style.customName;
|
||||||
|
resetEl.onclick = () => {
|
||||||
|
const {style} = editor;
|
||||||
|
nameEl.focus();
|
||||||
|
nameEl.select();
|
||||||
|
// trying to make it undoable via Ctrl-Z
|
||||||
|
if (!document.execCommand('insertText', false, style.name)) {
|
||||||
|
nameEl.value = style.name;
|
||||||
|
editor.updateName(true);
|
||||||
|
}
|
||||||
|
style.customName = null; // to delete it from db
|
||||||
|
resetEl.hidden = true;
|
||||||
|
};
|
||||||
|
const enabledEl = $('#enabled');
|
||||||
|
enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initThemeElement() {
|
||||||
|
$('#editor.theme').append(...[
|
||||||
|
$create('option', {value: 'default'}, t('defaultTheme')),
|
||||||
|
...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(async tab => {
|
||||||
|
ownTabId = tab.id;
|
||||||
|
// use browser history back when 'back to manage' is clicked
|
||||||
|
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
|
||||||
|
await baseInit.domReady;
|
||||||
|
$('#cancel-button').onclick = event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
history.back();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function initWindowedMode() {
|
||||||
|
chrome.tabs.onAttached.addListener(onTabAttached);
|
||||||
|
const isSimple = (await browser.windows.getCurrent()).type === 'popup';
|
||||||
|
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 notifyChange = wasDirty => {
|
||||||
|
if (wasDirty !== (data.size > 0)) {
|
||||||
|
listeners.forEach(cb => cb());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/** @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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyChange(wasDirty);
|
||||||
|
},
|
||||||
|
clear(obj) {
|
||||||
|
const wasDirty = data.size > 0;
|
||||||
|
if (obj === undefined) {
|
||||||
|
data.clear();
|
||||||
|
} else {
|
||||||
|
data.delete(obj);
|
||||||
|
}
|
||||||
|
notifyChange(wasDirty);
|
||||||
|
},
|
||||||
|
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 if (saved.type === 'modify') {
|
||||||
|
if (saved.savedValue === newValue) {
|
||||||
|
data.delete(obj);
|
||||||
|
} else {
|
||||||
|
saved.newValue = newValue;
|
||||||
|
}
|
||||||
|
} else if (saved.type === 'add') {
|
||||||
|
saved.newValue = newValue;
|
||||||
|
}
|
||||||
|
notifyChange(wasDirty);
|
||||||
|
},
|
||||||
|
onChange(cb, add = true) {
|
||||||
|
listeners[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';
|
||||||
|
}
|
||||||
|
notifyChange(wasDirty);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
315
edit/beautify.js
315
edit/beautify.js
|
|
@ -1,20 +1,18 @@
|
||||||
/* global loadScript css_beautify showHelp prefs t $ $create */
|
/* global $ $create moveFocus */// dom.js
|
||||||
/* global editor createHotkeyInput moveFocus CodeMirror */
|
/* global CodeMirror */
|
||||||
/* exported initBeautifyButton */
|
/* global createHotkeyInput helpPopup */// util.js
|
||||||
|
/* global editor */
|
||||||
|
/* global prefs */
|
||||||
|
/* global t */// localization.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const HOTKEY_ID = 'editor.beautify.hotkey';
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
prefs.initializing.then(() => {
|
prefs.subscribe('editor.beautify.hotkey', (key, value) => {
|
||||||
CodeMirror.defaults.extraKeys[prefs.get(HOTKEY_ID) || ''] = 'beautify';
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
prefs.subscribe([HOTKEY_ID], (key, value) => {
|
|
||||||
const {extraKeys} = CodeMirror.defaults;
|
const {extraKeys} = CodeMirror.defaults;
|
||||||
for (const [key, cmd] of Object.entries(extraKeys)) {
|
for (const [key, cmd] of Object.entries(extraKeys)) {
|
||||||
if (cmd === 'beautify') {
|
if (cmd === 'beautify') {
|
||||||
|
|
@ -25,164 +23,151 @@ prefs.subscribe([HOTKEY_ID], (key, value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
extraKeys[value] = 'beautify';
|
extraKeys[value] = 'beautify';
|
||||||
}
|
}
|
||||||
});
|
}, {runNow: true});
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} btn - the button element shown in the UI
|
|
||||||
* @param {function():CodeMirror[]} getScope
|
|
||||||
*/
|
|
||||||
function initBeautifyButton(btn, getScope) {
|
|
||||||
btn.addEventListener('click', () => beautify(getScope()));
|
|
||||||
btn.addEventListener('contextmenu', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
beautify(getScope(), false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @name beautify
|
||||||
* @param {CodeMirror[]} scope
|
* @param {CodeMirror[]} scope
|
||||||
* @param {?boolean} ui
|
* @param {boolean} [ui=true]
|
||||||
*/
|
*/
|
||||||
function beautify(scope, ui = true) {
|
async function beautify(scope, ui = true) {
|
||||||
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
await require(['/vendor-overwrites/beautify/beautify-css-mod']); /* global css_beautify */
|
||||||
.then(() => {
|
const tabs = prefs.get('editor.indentWithTabs');
|
||||||
if (!window.css_beautify && window.exports) {
|
const options = Object.assign(prefs.defaults['editor.beautify'], prefs.get('editor.beautify'));
|
||||||
window.css_beautify = window.exports.css_beautify;
|
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
||||||
}
|
options.indent_char = tabs ? '\t' : ' ';
|
||||||
})
|
if (ui) {
|
||||||
.then(doBeautify);
|
createBeautifyUI(scope, options);
|
||||||
|
}
|
||||||
|
for (const cm of scope) {
|
||||||
|
setTimeout(beautifyEditor, 0, cm, options, ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function doBeautify() {
|
function beautifyEditor(cm, options, ui) {
|
||||||
const tabs = prefs.get('editor.indentWithTabs');
|
const pos = options.translate_positions =
|
||||||
const options = Object.assign({}, prefs.get('editor.beautify'));
|
[].concat.apply([], cm.doc.sel.ranges.map(r =>
|
||||||
for (const k of Object.keys(prefs.defaults['editor.beautify'])) {
|
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
|
||||||
if (!(k in options)) options[k] = prefs.defaults['editor.beautify'][k];
|
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 = {};
|
||||||
}
|
}
|
||||||
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
cm.setValue(newText);
|
||||||
options.indent_char = tabs ? '\t' : ' ';
|
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) {
|
if (ui) {
|
||||||
createBeautifyUI(scope, options);
|
$('#help-popup button[role="close"]').disabled = false;
|
||||||
}
|
|
||||||
for (const cm of scope) {
|
|
||||||
setTimeout(doBeautifyEditor, 0, cm, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doBeautifyEditor(cm, options) {
|
|
||||||
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) {
|
|
||||||
$('#help-popup button[role="close"]').disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBeautifyUI(scope, options) {
|
|
||||||
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('p.beautify-hint', [
|
|
||||||
$create('span', t('styleBeautifyHint') + '\u00A0'),
|
|
||||||
createHotkeyInput(HOTKEY_ID, () => moveFocus($('#help-popup'), 1)),
|
|
||||||
]),
|
|
||||||
$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')),
|
|
||||||
]),
|
|
||||||
]));
|
|
||||||
|
|
||||||
$('#help-popup').className = 'wide';
|
|
||||||
|
|
||||||
$('.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'),
|
|
||||||
]),
|
|
||||||
$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) {
|
|
||||||
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),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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'),
|
||||||
|
]),
|
||||||
|
$create('p.beautify-hint', [
|
||||||
|
$create('span', t('styleBeautifyHint') + '\u00A0'),
|
||||||
|
createHotkeyInput('editor.beautify.hotkey', {
|
||||||
|
buttons: false,
|
||||||
|
onDone: () => moveFocus($('#help-popup'), 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')),
|
||||||
|
]),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$('#help-popup').className = 'wide';
|
||||||
|
|
||||||
|
$('.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());
|
||||||
|
}
|
||||||
|
beautify(scope, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
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',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,3 +1,5 @@
|
||||||
|
/* Built-in CodeMirror and addon customization */
|
||||||
|
|
||||||
.CodeMirror-hints {
|
.CodeMirror-hints {
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
@ -14,18 +16,9 @@
|
||||||
/* Not using the ring-color hack as it became ugly in new Chrome */
|
/* Not using the ring-color hack as it became ugly in new Chrome */
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
.CodeMirror-lint-mark-warning {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
.CodeMirror-dialog {
|
.CodeMirror-dialog {
|
||||||
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
||||||
}
|
}
|
||||||
.CodeMirror-bookmark {
|
|
||||||
background: linear-gradient(to right, currentColor, transparent);
|
|
||||||
position: absolute;
|
|
||||||
width: 2em;
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
.CodeMirror-search-field {
|
.CodeMirror-search-field {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|
@ -35,10 +28,6 @@
|
||||||
.CodeMirror-search-hint {
|
.CodeMirror-search-hint {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.cm-uso-variable {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-activeline .applies-to:before {
|
.CodeMirror-activeline .applies-to:before {
|
||||||
background-color: hsla(214, 100%, 90%, 0.15);
|
background-color: hsla(214, 100%, 90%, 0.15);
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -49,11 +38,9 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-activeline .applies-to ul {
|
.CodeMirror-activeline .applies-to ul {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldgutter-open::after,
|
.CodeMirror-foldgutter-open::after,
|
||||||
.CodeMirror-foldgutter-folded::after {
|
.CodeMirror-foldgutter-folded::after {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
|
@ -65,15 +52,25 @@
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
left: 1px;
|
left: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldgutter-open::after {
|
.CodeMirror-foldgutter-open::after {
|
||||||
border-width: 5px 3px 0 3px;
|
border-width: 5px 3px 0 3px;
|
||||||
border-color: currentColor transparent transparent transparent;
|
border-color: currentColor transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldgutter-folded::after {
|
.CodeMirror-foldgutter-folded::after {
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
border-width: 4px 0 4px 5px;
|
border-width: 4px 0 4px 5px;
|
||||||
border-color: transparent transparent transparent currentColor;
|
border-color: transparent transparent transparent currentColor;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
cursor: pointer; /* for bookmarking */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
/* global CodeMirror prefs loadScript editor $ template */
|
/* global $ */// dom.js
|
||||||
|
/* global CodeMirror */
|
||||||
|
/* global editor */
|
||||||
|
/* global prefs */
|
||||||
|
/* global t */// localization.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function () {
|
(() => {
|
||||||
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
||||||
if (!prefs.get('editor.keyMap')) {
|
if (!prefs.get('editor.keyMap')) {
|
||||||
prefs.reset('editor.keyMap');
|
prefs.reset('editor.keyMap');
|
||||||
}
|
}
|
||||||
|
|
||||||
const CM_BOOKMARK = 'CodeMirror-bookmark';
|
|
||||||
const CM_BOOKMARK_GUTTER = CM_BOOKMARK + 'gutter';
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
||||||
mode: 'css',
|
mode: 'css',
|
||||||
|
|
@ -17,7 +18,6 @@
|
||||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
lineWrapping: prefs.get('editor.lineWrapping'),
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
gutters: [
|
gutters: [
|
||||||
CM_BOOKMARK_GUTTER,
|
|
||||||
'CodeMirror-linenumbers',
|
'CodeMirror-linenumbers',
|
||||||
'CodeMirror-foldgutter',
|
'CodeMirror-foldgutter',
|
||||||
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
theme: prefs.get('editor.theme'),
|
theme: prefs.get('editor.theme'),
|
||||||
keyMap: prefs.get('editor.keyMap'),
|
keyMap: prefs.get('editor.keyMap'),
|
||||||
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
||||||
// independent of current keyMap
|
// independent of current keyMap; some are implemented only for the edit page
|
||||||
'Alt-Enter': 'toggleStyle',
|
'Alt-Enter': 'toggleStyle',
|
||||||
'Alt-PageDown': 'nextEditor',
|
'Alt-PageDown': 'nextEditor',
|
||||||
'Alt-PageUp': 'prevEditor',
|
'Alt-PageUp': 'prevEditor',
|
||||||
|
|
@ -40,323 +40,107 @@
|
||||||
|
|
||||||
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
||||||
|
|
||||||
// 'basic' keymap only has basic keys by design, so we skip it
|
// Adding hotkeys to some keymaps except 'basic' which is primitive by design
|
||||||
|
require(Object.values(typeof editor === 'object' && editor.lazyKeymaps || {}), () => {
|
||||||
const extraKeysCommands = {};
|
const KM = CodeMirror.keyMap;
|
||||||
Object.keys(CodeMirror.defaults.extraKeys).forEach(key => {
|
const extras = Object.values(CodeMirror.defaults.extraKeys);
|
||||||
extraKeysCommands[CodeMirror.defaults.extraKeys[key]] = true;
|
if (!extras.includes('jumpToLine')) {
|
||||||
});
|
KM.sublime['Ctrl-G'] = 'jumpToLine';
|
||||||
if (!extraKeysCommands.jumpToLine) {
|
KM.emacsy['Ctrl-G'] = 'jumpToLine';
|
||||||
CodeMirror.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
|
KM.pcDefault['Ctrl-J'] = 'jumpToLine';
|
||||||
CodeMirror.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
|
KM.macDefault['Cmd-J'] = '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';
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if (!extras.includes('autocomplete')) {
|
||||||
CodeMirror.keyMap.pcDefault['Shift-F3'] = 'findPrev';
|
// 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 (!extraKeysCommands.replace) {
|
if (!extras.includes('blockComment')) {
|
||||||
CodeMirror.keyMap.pcDefault['Ctrl-R'] = 'replace';
|
KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
|
||||||
}
|
}
|
||||||
|
if (navigator.appVersion.includes('Windows')) {
|
||||||
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
|
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
|
||||||
['N', 'T', 'W'].forEach(char => {
|
if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
|
||||||
[
|
if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
|
||||||
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
if (!extras.includes('replace')) KM.pcDefault['Ctrl-R'] = 'replace';
|
||||||
// Note: modifier order in CodeMirror is S-C-A
|
// try to remap non-interceptable (Shift-)Ctrl-N/T/W hotkeys
|
||||||
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']}
|
// Note: modifier order in CodeMirror is S-C-A
|
||||||
].forEach(remap => {
|
for (const char of ['N', 'T', 'W']) {
|
||||||
const oldKey = remap.from + char;
|
for (const remap of [
|
||||||
Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
|
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
||||||
const keyMap = CodeMirror.keyMap[keyMapName];
|
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
|
||||||
const command = keyMap[oldKey];
|
]) {
|
||||||
if (!command) {
|
const oldKey = remap.from + char;
|
||||||
return;
|
for (const km of Object.values(KM)) {
|
||||||
}
|
const command = km[oldKey];
|
||||||
remap.to.some(newMod => {
|
if (!command) continue;
|
||||||
const newKey = newMod + char;
|
for (const newMod of remap.to) {
|
||||||
if (!(newKey in keyMap)) {
|
const newKey = newMod + char;
|
||||||
delete keyMap[oldKey];
|
if (newKey in km) continue;
|
||||||
keyMap[newKey] = command;
|
km[newKey] = command;
|
||||||
return true;
|
delete km[oldKey];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
|
|
||||||
'content-visibility': true,
|
|
||||||
'overflow-anchor': true,
|
|
||||||
'overscroll-behavior': true,
|
|
||||||
});
|
|
||||||
Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, {
|
|
||||||
'darkgrey': true,
|
|
||||||
'darkslategrey': true,
|
|
||||||
'dimgrey': true,
|
|
||||||
'lightgrey': true,
|
|
||||||
'lightslategrey': true,
|
|
||||||
'slategrey': true,
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const elBookmark = document.createElement('div');
|
|
||||||
elBookmark.className = CM_BOOKMARK;
|
|
||||||
elBookmark.textContent = '\u00A0';
|
|
||||||
const clearMarker = function () {
|
|
||||||
const line = this.lines[0];
|
|
||||||
CodeMirror.TextMarker.prototype.clear.apply(this);
|
|
||||||
if (!line.markedSpans.some(span => span.marker.sublimeBookmark)) {
|
|
||||||
this.doc.setGutterMarker(line, CM_BOOKMARK_GUTTER, null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const {markText} = CodeMirror.prototype;
|
|
||||||
Object.assign(CodeMirror.prototype, {
|
Object.assign(CodeMirror.prototype, {
|
||||||
markText() {
|
/**
|
||||||
const marker = markText.apply(this, arguments);
|
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
|
||||||
if (marker.sublimeBookmark) {
|
* @param {boolean} [force]
|
||||||
this.doc.setGutterMarker(marker.lines[0], CM_BOOKMARK_GUTTER, elBookmark.cloneNode(true));
|
*/
|
||||||
marker.clear = clearMarker;
|
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);
|
||||||
}
|
}
|
||||||
return marker;
|
},
|
||||||
|
/** 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();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// CodeMirror convenience commands
|
|
||||||
Object.assign(CodeMirror.commands, {
|
Object.assign(CodeMirror.commands, {
|
||||||
toggleEditorFocus,
|
jumpToLine(cm) {
|
||||||
jumpToLine,
|
const cur = cm.getCursor();
|
||||||
commentSelection,
|
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});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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 RX_CONSUME_PROP = /[-\w]*\s*:\s?|$/y;
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
// add ":" after a property name
|
|
||||||
const res = originalHelper(cm);
|
|
||||||
const state = res && cm.getTokenAt(pos).state.state;
|
|
||||||
if (state === 'block' || state === 'maybeprop') {
|
|
||||||
res.list = res.list.map(str => str + ': ');
|
|
||||||
RX_CONSUME_PROP.lastIndex = res.to.ch;
|
|
||||||
res.to.ch += RX_CONSUME_PROP.exec(text)[0].length;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// USO vars in usercss mode editor
|
|
||||||
const vars = editor.style.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.style.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,86 +1,201 @@
|
||||||
/* global CodeMirror loadScript rerouteHotkeys prefs $ debounce $create */
|
/* global $ */// dom.js
|
||||||
/* exported cmFactory */
|
/* global CodeMirror */
|
||||||
|
/* global editor */
|
||||||
|
/* global prefs */
|
||||||
|
/* global rerouteHotkeys */// util.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
All cm instances created by this module are collected so we can broadcast prefs
|
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
|
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
||||||
when the instance is not used anymore.
|
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;
|
|
||||||
|
|
||||||
CodeMirror.defineOption('tabSize', prefs.get('editor.tabSize'), (cm, value) => {
|
(() => {
|
||||||
cm.setOption('indentUnit', Number(value));
|
//#region Factory
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineOption('indentWithTabs', prefs.get('editor.indentWithTabs'), (cm, value) => {
|
const cms = new Set();
|
||||||
CodeMirror.commands.insertTab = value ?
|
let lazyOpt;
|
||||||
INSERT_TAB_COMMAND :
|
|
||||||
INSERT_SOFT_TAB_COMMAND;
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineOption('autocompleteOnTyping', prefs.get('editor.autocompleteOnTyping'), (cm, value) => {
|
const cmFactory = window.cmFactory = {
|
||||||
const onOff = value ? 'on' : 'off';
|
|
||||||
cm[onOff]('changes', autocompleteOnTyping);
|
|
||||||
cm[onOff]('pick', autocompletePicked);
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineOption('matchHighlight', prefs.get('editor.matchHighlight'), (cm, value) => {
|
create(place, options) {
|
||||||
if (value === 'token') {
|
const cm = CodeMirror(place, options);
|
||||||
cm.setOption('highlightSelectionMatches', {
|
cm.lastActive = 0;
|
||||||
showToken: /[#.\-\w]/,
|
cms.add(cm);
|
||||||
annotateScrollbar: true,
|
return cm;
|
||||||
onUpdate: updateMatchHighlightCount
|
},
|
||||||
});
|
|
||||||
} else if (value === 'selection') {
|
|
||||||
cm.setOption('highlightSelectionMatches', {
|
|
||||||
showToken: false,
|
|
||||||
annotateScrollbar: true,
|
|
||||||
onUpdate: updateMatchHighlightCount
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cm.setOption('highlightSelectionMatches', null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineOption('selectByTokens', prefs.get('editor.selectByTokens'), (cm, value) => {
|
destroy(cm) {
|
||||||
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
cms.delete(cm);
|
||||||
});
|
},
|
||||||
|
|
||||||
prefs.subscribe(null, (key, value) => {
|
globalSetOption(key, value) {
|
||||||
const option = key.replace(/^editor\./, '');
|
CodeMirror.defaults[key] = value;
|
||||||
if (!option) {
|
if (cms.size > 4 && lazyOpt && lazyOpt.names.includes(key)) {
|
||||||
console.error('no "cm_option"', key);
|
lazyOpt.set(key, value);
|
||||||
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 {
|
} else {
|
||||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
cms.forEach(cm => cm.setOption(key, value));
|
||||||
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();
|
// focus and blur
|
||||||
newThemeLink.id = 'cm-theme';
|
|
||||||
});
|
const onCmFocus = cm => {
|
||||||
|
rerouteHotkeys.toggle(false);
|
||||||
|
cm.display.wrapper.classList.add('CodeMirror-active');
|
||||||
|
cm.lastActive = Date.now();
|
||||||
|
};
|
||||||
|
const onCmBlur = cm => {
|
||||||
|
rerouteHotkeys.toggle(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
const {wrapper} = cm.display;
|
||||||
|
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
CodeMirror.defineInitHook(cm => {
|
||||||
|
cm.on('focus', onCmFocus);
|
||||||
|
cm.on('blur', onCmBlur);
|
||||||
|
});
|
||||||
|
|
||||||
|
// propagated preferences
|
||||||
|
|
||||||
|
const prefToCmOpt = k =>
|
||||||
|
k.startsWith('editor.') &&
|
||||||
|
k.slice('editor.'.length);
|
||||||
|
const prefKeys = prefs.knownKeys.filter(k =>
|
||||||
|
k !== 'editor.colorpicker' && // handled in colorpicker-helper.js
|
||||||
|
prefToCmOpt(k) in CodeMirror.defaults);
|
||||||
|
const {insertTab, insertSoftTab} = CodeMirror.commands;
|
||||||
|
|
||||||
|
for (const [key, fn] of Object.entries({
|
||||||
|
'editor.tabSize'(cm, value) {
|
||||||
|
cm.setOption('indentUnit', Number(value));
|
||||||
|
},
|
||||||
|
'editor.indentWithTabs'(cm, value) {
|
||||||
|
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
|
||||||
|
},
|
||||||
|
'editor.matchHighlight'(cm, value) {
|
||||||
|
const showToken = value === 'token' && /[#.\-\w]/;
|
||||||
|
const opt = (showToken || value === 'selection') && {
|
||||||
|
showToken,
|
||||||
|
annotateScrollbar: true,
|
||||||
|
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) => {
|
||||||
|
const name = prefToCmOpt(key);
|
||||||
|
if (name === 'theme') {
|
||||||
|
loadCmTheme(val);
|
||||||
|
} else {
|
||||||
|
cmFactory.globalSetOption(name, val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// lazy propagation
|
||||||
|
|
||||||
|
lazyOpt = window.IntersectionObserver && {
|
||||||
|
names: ['theme', 'lineWrapping'],
|
||||||
|
set(key, value) {
|
||||||
|
const {observer, queue} = lazyOpt;
|
||||||
|
for (const cm of cms) {
|
||||||
|
let opts = queue.get(cm);
|
||||||
|
if (!opts) queue.set(cm, opts = {});
|
||||||
|
opts[key] = value;
|
||||||
|
observer.observe(cm.display.wrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setNow({cm, data}) {
|
||||||
|
cm.operation(() => data.forEach(kv => cm.setOption(...kv)));
|
||||||
|
},
|
||||||
|
onView(entries) {
|
||||||
|
const {queue, observer} = lazyOpt;
|
||||||
|
const delayed = [];
|
||||||
|
for (const e of entries) {
|
||||||
|
const r = e.isIntersecting && e.intersectionRect;
|
||||||
|
if (!r) continue;
|
||||||
|
const cm = e.target.CodeMirror;
|
||||||
|
const data = Object.entries(queue.get(cm) || {});
|
||||||
|
queue.delete(cm);
|
||||||
|
observer.unobserve(e.target);
|
||||||
|
if (!data.every(([key, val]) => cm.getOption(key) === val)) {
|
||||||
|
if (r.bottom > 0 && r.top < window.innerHeight) {
|
||||||
|
lazyOpt.setNow({cm, data});
|
||||||
|
} else {
|
||||||
|
delayed.push({cm, data});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (delayed.length) {
|
||||||
// broadcast option
|
setTimeout(() => delayed.forEach(lazyOpt.setNow));
|
||||||
setOption(option, value);
|
}
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return {create, destroy, setOption};
|
for (const cmd of [
|
||||||
|
'nextEditor',
|
||||||
|
'prevEditor',
|
||||||
|
'save',
|
||||||
|
'toggleStyle',
|
||||||
|
]) {
|
||||||
|
CodeMirror.commands[cmd] = (...args) => editor[cmd](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region CM option handlers
|
||||||
|
|
||||||
|
async function loadCmTheme(name) {
|
||||||
|
let el2;
|
||||||
|
const el = $('#cm-theme');
|
||||||
|
if (name === 'default') {
|
||||||
|
el.href = '';
|
||||||
|
} else {
|
||||||
|
const path = `/vendor/codemirror/theme/${name}.css`;
|
||||||
|
if (el.href !== location.origin + path) {
|
||||||
|
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||||
|
el2 = await require([path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmFactory.globalSetOption('theme', name);
|
||||||
|
if (el2) {
|
||||||
|
el.remove();
|
||||||
|
el2.id = el.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateMatchHighlightCount(cm, state) {
|
function updateMatchHighlightCount(cm, state) {
|
||||||
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
||||||
|
|
@ -143,151 +258,76 @@ const cmFactory = (() => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function autocompleteOnTyping(cm, [info], debounced) {
|
//#endregion
|
||||||
const lastLine = info.text[info.text.length - 1];
|
//#region Bookmarks
|
||||||
if (
|
|
||||||
cm.state.completionActive ||
|
const BM_CLS = 'gutter-bookmark';
|
||||||
info.origin && !info.origin.includes('input') ||
|
const BM_BRAND = 'sublimeBookmark';
|
||||||
!lastLine
|
const BM_CLICKER = 'CodeMirror-linenumbers';
|
||||||
) {
|
const BM_DATA = Symbol('data');
|
||||||
return;
|
// TODO: revisit when https://github.com/codemirror/CodeMirror/issues/6716 is fixed
|
||||||
}
|
const tmProto = CodeMirror.TextMarker.prototype;
|
||||||
if (cm.state.autocompletePicked) {
|
const tmProtoOvr = {};
|
||||||
cm.state.autocompletePicked = false;
|
for (const k of ['clear', 'attachLine', 'detachLine']) {
|
||||||
return;
|
tmProtoOvr[k] = function (line) {
|
||||||
}
|
const {cm} = this.doc;
|
||||||
if (!debounced) {
|
const withOp = !cm.curOp;
|
||||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
if (withOp) cm.startOperation();
|
||||||
return;
|
tmProto[k].apply(this, arguments);
|
||||||
}
|
cm.curOp.ownsGroup.delayedCallbacks.push(toggleMark.bind(this, this.lines[0], line));
|
||||||
if (lastLine.match(/[-a-z!]+$/i)) {
|
if (withOp) cm.endOperation();
|
||||||
cm.state.autocompletePicked = false;
|
};
|
||||||
cm.options.hintOptions.completeSingle = false;
|
|
||||||
cm.execCommand('autocomplete');
|
|
||||||
setTimeout(() => {
|
|
||||||
cm.options.hintOptions.completeSingle = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
for (const name of ['prevBookmark', 'nextBookmark']) {
|
||||||
function autocompletePicked(cm) {
|
const cmdFn = CodeMirror.commands[name];
|
||||||
cm.state.autocompletePicked = true;
|
CodeMirror.commands[name] = cm => {
|
||||||
|
cm.setSelection = cm.jumpToPos;
|
||||||
|
cmdFn(cm);
|
||||||
|
delete cm.setSelection;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
CodeMirror.defineInitHook(cm => {
|
||||||
function destroy(cm) {
|
cm.on('gutterClick', onGutterClick);
|
||||||
editors.delete(cm);
|
cm.on('gutterContextMenu', onGutterContextMenu);
|
||||||
}
|
cm.on('markerAdded', onMarkAdded);
|
||||||
|
});
|
||||||
function create(init, options) {
|
// TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
|
||||||
const cm = CodeMirror(init, options);
|
function onGutterClick(cm, line, name, e) {
|
||||||
cm.lastActive = 0;
|
switch (name === BM_CLICKER && e.button) {
|
||||||
const wrapper = cm.display.wrapper;
|
case 0: {
|
||||||
cm.on('blur', () => {
|
// main button: toggle
|
||||||
rerouteHotkeys(true);
|
const [mark] = cm.findMarks({line, ch: 0}, {line, ch: 1e9}, m => m[BM_BRAND]);
|
||||||
setTimeout(() => {
|
cm.setCursor(mark ? mark.find(-1) : {line, ch: 0});
|
||||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
cm.execCommand('toggleBookmark');
|
||||||
});
|
|
||||||
});
|
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
|
case 1:
|
||||||
|
// middle button: select all marks
|
||||||
|
cm.execCommand('selectBookmarks');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (index >= total) {
|
|
||||||
$.remove(progress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!progress &&
|
|
||||||
index < total / 2 &&
|
|
||||||
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
|
||||||
let option = $('#editor.' + key);
|
|
||||||
if (option) {
|
|
||||||
if (option.type === 'checkbox') {
|
|
||||||
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
|
||||||
}
|
|
||||||
progress = document.body.appendChild(
|
|
||||||
$create('.set-option-progress', {targetElement: option}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (progress) {
|
|
||||||
const optionBounds = progress.targetElement.getBoundingClientRect();
|
|
||||||
const bounds = {
|
|
||||||
top: optionBounds.top + window.scrollY + 1,
|
|
||||||
left: optionBounds.left + window.scrollX + 1,
|
|
||||||
width: (optionBounds.width - 2) * index / total | 0,
|
|
||||||
height: optionBounds.height - 2,
|
|
||||||
};
|
|
||||||
const style = progress.style;
|
|
||||||
for (const prop in bounds) {
|
|
||||||
if (bounds[prop] !== parseFloat(style[prop])) {
|
|
||||||
style[prop] = bounds[prop] + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTimeout(throttleSetOption, 0, {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
timeStart,
|
|
||||||
cmStart,
|
|
||||||
editorsCopy,
|
|
||||||
progress,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
function onGutterContextMenu(cm, line, name, e) {
|
||||||
|
if (name === BM_CLICKER) {
|
||||||
|
cm.execCommand(e.ctrlKey ? 'prevBookmark' : 'nextBookmark');
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onMarkAdded(cm, mark) {
|
||||||
|
if (mark[BM_BRAND]) {
|
||||||
|
// CM bug workaround to keep the mark at line start when the above line is removed
|
||||||
|
mark.inclusiveRight = true;
|
||||||
|
Object.assign(mark, tmProtoOvr);
|
||||||
|
toggleMark.call(mark, true, mark[BM_DATA] = mark.lines[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggleMark(state, line = this[BM_DATA]) {
|
||||||
|
this.doc[state ? 'addLineClass' : 'removeLineClass'](line, 'gutter', BM_CLS);
|
||||||
|
if (state) {
|
||||||
|
const bms = this.doc.cm.state.sublimeBookmarks;
|
||||||
|
if (!bms.includes(this)) bms.push(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
/* exported CODEMIRROR_THEMES */
|
/* Do not edit. This file is auto-generated by build-vendor.js */
|
||||||
// this file is generated by update-codemirror-themes.js
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/* exported CODEMIRROR_THEMES */
|
||||||
const CODEMIRROR_THEMES = [
|
const CODEMIRROR_THEMES = [
|
||||||
'3024-day',
|
'3024-day',
|
||||||
'3024-night',
|
'3024-night',
|
||||||
|
'abbott',
|
||||||
'abcdef',
|
'abcdef',
|
||||||
'ambiance',
|
'ambiance',
|
||||||
'ambiance-mobile',
|
'ambiance-mobile',
|
||||||
|
|
@ -28,6 +29,7 @@ const CODEMIRROR_THEMES = [
|
||||||
'icecoder',
|
'icecoder',
|
||||||
'idea',
|
'idea',
|
||||||
'isotope',
|
'isotope',
|
||||||
|
'juejin',
|
||||||
'lesser-dark',
|
'lesser-dark',
|
||||||
'liquibyte',
|
'liquibyte',
|
||||||
'lucario',
|
'lucario',
|
||||||
|
|
@ -65,5 +67,5 @@ const CODEMIRROR_THEMES = [
|
||||||
'xq-light',
|
'xq-light',
|
||||||
'yeti',
|
'yeti',
|
||||||
'yonce',
|
'yonce',
|
||||||
'zenburn'
|
'zenburn',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/* global CodeMirror showHelp cmFactory onDOMready $ prefs t createHotkeyInput */
|
|
||||||
'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 = {
|
|
||||||
tooltip: t('colorpickerTooltip'),
|
|
||||||
popup: {
|
|
||||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
|
||||||
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
|
|
||||||
hideDelay: 30e3,
|
|
||||||
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 = createHotkeyInput('editor.colorpicker.hotkey', () => {
|
|
||||||
$('#help-popup .dismiss').onclick();
|
|
||||||
});
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
346
edit/edit.css
346
edit/edit.css
|
|
@ -1,12 +1,21 @@
|
||||||
:root {
|
:root {
|
||||||
--header-narrow-min-height: 12em;
|
--fixed-padding: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
font: 12px arial,sans-serif;
|
font: 12px arial,sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #000;
|
||||||
|
transition: color .5s;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
#global-progress {
|
#global-progress {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
|
|
@ -18,15 +27,52 @@ body {
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 2s;
|
transition: opacity 2s;
|
||||||
|
contain: strict;
|
||||||
}
|
}
|
||||||
#global-progress[title] {
|
#global-progress[title] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.is-new-style #preview-label,
|
||||||
|
html.is-new-style #publish,
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
html.is-new-style #heading::after {
|
||||||
|
content: attr(data-add);
|
||||||
|
}
|
||||||
|
html:not(.is-new-style) #heading::after {
|
||||||
|
content: attr(data-edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/************ embedded popup for simple-window editor ************/
|
||||||
|
#popup-iframe {
|
||||||
|
max-height: 600px;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
border: none;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 30px #000;
|
||||||
|
}
|
||||||
|
#popup-iframe:not([data-loaded]) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
#popup-button {
|
||||||
|
position: fixed;
|
||||||
|
right: 7px;
|
||||||
|
top: 11px;
|
||||||
|
z-index: 1000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter .25s;
|
||||||
|
}
|
||||||
|
#popup-button:hover {
|
||||||
|
filter: drop-shadow(0 0 3px hsl(180, 70%, 50%));
|
||||||
|
}
|
||||||
|
.usercss body:not(.compact-layout) #popup-button {
|
||||||
|
right: 24px;
|
||||||
|
}
|
||||||
/************ checkbox & select************/
|
/************ checkbox & select************/
|
||||||
.options-column > div[class="option"] {
|
.options-column > div[class="option"] {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
@ -135,9 +181,6 @@ label {
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
cursor: pointer;
|
|
||||||
vertical-align: middle;
|
|
||||||
transition: fill .5s;
|
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
@ -146,9 +189,6 @@ label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
#mozilla-format-heading .svg-inline-wrapper {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
#colorpicker-settings.svg-inline-wrapper {
|
#colorpicker-settings.svg-inline-wrapper {
|
||||||
margin: -2px 0 0 .1rem;
|
margin: -2px 0 0 .1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -190,10 +230,10 @@ input:invalid {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: -13px;
|
margin-left: -13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: .5rem;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
}
|
||||||
|
#header summary + * {
|
||||||
|
padding: .5rem 0;
|
||||||
|
}
|
||||||
#header summary h2 {
|
#header summary h2 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-bottom: 1px dotted transparent;
|
border-bottom: 1px dotted transparent;
|
||||||
|
|
@ -211,18 +251,25 @@ input:invalid {
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#details-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header details[open] + details[open] {
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
#actions > * {
|
#actions > * {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mozilla-format-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mozilla-format-buttons {
|
#mozilla-format-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#actions > div > a {
|
#actions > div > a {
|
||||||
|
|
@ -244,6 +291,81 @@ input:invalid {
|
||||||
#lint:not([open]) h2 {
|
#lint:not([open]) h2 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#publish > div > * {
|
||||||
|
margin-top: .75em;
|
||||||
|
}
|
||||||
|
#publish a:visited {
|
||||||
|
margin-top: .75em;
|
||||||
|
}
|
||||||
|
#publish[data-connected] summary::marker,
|
||||||
|
#publish[data-connected] h2 {
|
||||||
|
color: hsl(180, 100%, 20%);
|
||||||
|
}
|
||||||
|
#publish:not([data-connected]) #usw-link-info,
|
||||||
|
#publish:not([data-connected]) #usw-disconnect {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#publish[data-connected] #usw-publish-style::after {
|
||||||
|
content: attr(data-push);
|
||||||
|
}
|
||||||
|
#publish:not([data-connected]) #usw-publish-style::after {
|
||||||
|
content: attr(data-publish);
|
||||||
|
}
|
||||||
|
#usw-link-info dl {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#usw-link-info dt {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#usw-link-info dt::after {
|
||||||
|
content: ":"
|
||||||
|
}
|
||||||
|
#usw-link-info dt,
|
||||||
|
#usw-link-info dd {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#usw-link-info dd {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
#usw-link-info dd[data-usw="name"] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#usw-progress {
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
#usw-progress .success,
|
||||||
|
#usw-progress .unchanged {
|
||||||
|
font-size: 150%;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: .25em;
|
||||||
|
}
|
||||||
|
#usw-progress .success {
|
||||||
|
margin-top: -.25em;
|
||||||
|
}
|
||||||
|
#usw-progress .success::after {
|
||||||
|
content: '\2713'; /* checkmark */
|
||||||
|
}
|
||||||
|
#usw-progress .unchanged::after {
|
||||||
|
content: '=';
|
||||||
|
}
|
||||||
|
#usw-progress .error {
|
||||||
|
display: block;
|
||||||
|
margin-top: .5em;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
#usw-progress .error + div {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
#usw-progress .lds-spinner {
|
||||||
|
transform: scale(0.125);
|
||||||
|
transform-origin: 0 10px;
|
||||||
|
}
|
||||||
/* options */
|
/* options */
|
||||||
#options [type="number"] {
|
#options [type="number"] {
|
||||||
width: 3.5em;
|
width: 3.5em;
|
||||||
|
|
@ -254,12 +376,6 @@ input:invalid {
|
||||||
padding: .1rem .25rem 0 0;
|
padding: .1rem .25rem 0 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.set-option-progress {
|
|
||||||
position: absolute;
|
|
||||||
background-color: currentColor;
|
|
||||||
content: "";
|
|
||||||
opacity: .15;
|
|
||||||
}
|
|
||||||
/* footer */
|
/* footer */
|
||||||
.usercss #footer {
|
.usercss #footer {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -272,6 +388,7 @@ input:invalid {
|
||||||
/************ section editor ***********/
|
/************ section editor ***********/
|
||||||
.CodeMirror-vscrollbar,
|
.CodeMirror-vscrollbar,
|
||||||
.CodeMirror-hscrollbar {
|
.CodeMirror-hscrollbar {
|
||||||
|
box-shadow: none !important;
|
||||||
pointer-events: auto !important; /* FF bug */
|
pointer-events: auto !important; /* FF bug */
|
||||||
}
|
}
|
||||||
.section-editor .section {
|
.section-editor .section {
|
||||||
|
|
@ -304,7 +421,10 @@ input:invalid {
|
||||||
#sections {
|
#sections {
|
||||||
counter-reset: codebox;
|
counter-reset: codebox;
|
||||||
}
|
}
|
||||||
#sections > .section > label {
|
#sections > .section:not(.removed) > label {
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13px;
|
||||||
animation: 2s highlight;
|
animation: 2s highlight;
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
animation-direction: reverse;
|
animation-direction: reverse;
|
||||||
|
|
@ -312,9 +432,41 @@ input:invalid {
|
||||||
}
|
}
|
||||||
#sections > .section > label::after {
|
#sections > .section > label::after {
|
||||||
counter-increment: codebox;
|
counter-increment: codebox;
|
||||||
content: counter(codebox);
|
content: counter(codebox) ": " attr(data-text);
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
.single-editor .applies-to > label::before {
|
||||||
|
content: attr(data-index) ":";
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.code-label[data-text] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#toc {
|
||||||
|
counter-reset: codelabel;
|
||||||
|
margin: 0;
|
||||||
|
padding: .5rem 0;
|
||||||
|
}
|
||||||
|
#toc li {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#toc li.current:not(:only-child) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#toc li[tabindex="-1"] {
|
||||||
|
opacity: .25;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#toc li:hover {
|
||||||
|
background-color: hsla(180, 50%, 36%, .2);
|
||||||
|
}
|
||||||
|
#toc li[tabindex="0"]::before {
|
||||||
|
counter-increment: codelabel;
|
||||||
|
content: counter(codelabel) ": ";
|
||||||
|
}
|
||||||
.section:only-of-type .move-section-up,
|
.section:only-of-type .move-section-up,
|
||||||
.section:only-of-type .move-section-down {
|
.section:only-of-type .move-section-down {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -384,7 +536,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .cm-matchhighlight,
|
||||||
body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar {
|
body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
@-webkit-keyframes highlight {
|
@keyframes highlight {
|
||||||
from {
|
from {
|
||||||
background-color: #ff9;
|
background-color: #ff9;
|
||||||
}
|
}
|
||||||
|
|
@ -437,6 +589,13 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
background-color: #f7f7f7; /* .CodeMirror-gutters */
|
||||||
|
border: solid #bbb;
|
||||||
|
border-width: 1px 0;
|
||||||
|
}
|
||||||
|
.applies-to.error {
|
||||||
|
background-color: #f002;
|
||||||
|
border-color: #f008;
|
||||||
}
|
}
|
||||||
.applies-to label {
|
.applies-to label {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -520,7 +679,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.regexp-report details:not(:last-child) {
|
.regexp-report details {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.regexp-report summary {
|
.regexp-report summary {
|
||||||
|
|
@ -543,6 +702,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
|
.regexp-report details div {
|
||||||
|
max-height: calc(100vh - 15rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
.regexp-report .svg-icon {
|
.regexp-report .svg-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
|
|
@ -559,14 +722,13 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
.regexp-report details a img {
|
.regexp-report details a img {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
max-height: 16px;
|
max-height: 16px;
|
||||||
position: absolute;
|
vertical-align: middle;
|
||||||
margin-left: -20px;
|
margin-right: .5em;
|
||||||
margin-top: -1px;
|
|
||||||
}
|
}
|
||||||
.regexp-report-note {
|
.regexp-report-note {
|
||||||
color: #999;
|
color: #999;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: 0 0.5rem 0 0;
|
bottom: 0;
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
/************ help popup ************/
|
/************ help popup ************/
|
||||||
|
|
@ -617,9 +779,12 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
#help-popup .CodeMirror {
|
#help-popup .CodeMirror {
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
}
|
}
|
||||||
|
#help-popup .keymap-list input[type="search"] {
|
||||||
|
margin: 0 0 2px;
|
||||||
|
}
|
||||||
.keymap-list {
|
.keymap-list {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
padding: 0 3px 0 0;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
@ -637,6 +802,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
|
|
||||||
#help-popup .buttons {
|
#help-popup .buttons {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-top: .75em;
|
||||||
}
|
}
|
||||||
.non-windows #help-popup .buttons {
|
.non-windows #help-popup .buttons {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
|
|
@ -656,15 +822,21 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
#help-popup .rules {
|
#help-popup .rules {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
#help-popup button {
|
#help-popup .rules li {
|
||||||
margin-right: 3px;
|
padding-top: .5em;
|
||||||
|
}
|
||||||
|
#help-popup .rules p {
|
||||||
|
margin: .25em 0;
|
||||||
|
}
|
||||||
|
#help-popup .buttons button:nth-child(n + 2) {
|
||||||
|
margin-left: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************ lint ************/
|
/************ lint ************/
|
||||||
#lint {
|
#lint {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: .5rem -1rem 0;
|
margin-left: -1rem;
|
||||||
min-height: 30px;
|
margin-right: -1rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -677,13 +849,13 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
#lint[open]:not(.hidden-unless-compact) {
|
#lint[open]:not(.hidden-unless-compact) {
|
||||||
min-height: 130px;
|
min-height: 102px;
|
||||||
}
|
}
|
||||||
#lint summary h2 {
|
#lint summary h2 {
|
||||||
margin-left: -16px;
|
text-indent: -2px;
|
||||||
}
|
}
|
||||||
#lint > .lint-scroll-container {
|
#lint > .lint-scroll-container {
|
||||||
margin: 42px 1rem 0;
|
margin: 1rem 10px 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
@ -721,7 +893,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#lint tr:hover {
|
#lint tr:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: hsla(180, 50%, 36%, .2);
|
||||||
|
}
|
||||||
|
#lint td {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
#lint td[role="severity"] {
|
#lint td[role="severity"] {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
|
@ -729,7 +904,6 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
}
|
}
|
||||||
#lint td[role="line"], #lint td[role="sep"] {
|
#lint td[role="line"], #lint td[role="sep"] {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 0;
|
|
||||||
}
|
}
|
||||||
#lint td[role="col"] {
|
#lint td[role="col"] {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -742,6 +916,11 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
#message-box.center.lint-config #message-box-contents {
|
#message-box.center.lint-config #message-box-contents {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
#help-popup .active-linter-rule {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
background-color: rgba(128, 128, 128, .2);
|
||||||
|
}
|
||||||
|
|
||||||
/************ CSS beautifier ************/
|
/************ CSS beautifier ************/
|
||||||
.beautify-options {
|
.beautify-options {
|
||||||
|
|
@ -787,20 +966,12 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
||||||
}
|
}
|
||||||
|
|
||||||
/************ single editor **************/
|
/************ single editor **************/
|
||||||
.usercss body {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-items: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usercss .CodeMirror-focused {
|
.usercss .CodeMirror-focused {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not(.usercss) .usercss-only,
|
html:not(.usercss) .usercss-only,
|
||||||
.usercss #mozilla-format-container,
|
.usercss .sectioned-only {
|
||||||
.usercss #sections > h2 {
|
|
||||||
display: none !important; /* hide during page init */
|
display: none !important; /* hide during page init */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -817,13 +988,9 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
margin-top: .75rem;
|
margin-top: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-editor {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-editor .CodeMirror {
|
.single-editor .CodeMirror {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 100vh; /* WARNING! Don't use 100% as it's dead slow */
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
@ -837,26 +1004,16 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usercss.firefox #sections,
|
|
||||||
.usercss.firefox .CodeMirror {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************ line widget *************/
|
/************ line widget *************/
|
||||||
.CodeMirror-linewidget .applies-to {
|
.CodeMirror-linewidget .applies-to {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
padding: .75rem .75rem .25rem;
|
padding: .75rem calc(.25rem + var(--cm-bar-width, 0)) .25rem .75rem;
|
||||||
padding-right: calc(1em + 20px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-linewidget .applies-to li {
|
.CodeMirror-linewidget .applies-to li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-linewidget .applies-to li + li {
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-linewidget .applies-to li[data-type="regexp"] .test-regexp {
|
.CodeMirror-linewidget .applies-to li[data-type="regexp"] .test-regexp {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
@ -878,10 +1035,10 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
position: inherit;
|
position: inherit;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: 1px dashed #AAA;
|
border-bottom: 1px dashed #AAA;
|
||||||
padding: 0;
|
padding: .5rem 1rem .5rem .5rem;
|
||||||
}
|
}
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
padding-top: 40px;
|
padding-top: var(--fixed-padding);
|
||||||
}
|
}
|
||||||
.fixed-header #header {
|
.fixed-header #header {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
|
@ -889,30 +1046,37 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 8px 0 0;
|
padding: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
.fixed-header #header > *:not(#lint) {
|
.fixed-header #header > *:not(#details-wrapper),
|
||||||
|
.fixed-header #options {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
#header summary + *,
|
||||||
|
#lint > .lint-scroll-container {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: .25rem 0 .5rem;
|
||||||
|
}
|
||||||
#actions {
|
#actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0 1rem;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
#header input[type="checkbox"] {
|
#header input[type="checkbox"] {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
#header details {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
#heading,
|
#heading,
|
||||||
h2 {
|
h2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#basic-info {
|
#basic-info {
|
||||||
padding: .5rem 1rem;
|
margin-bottom: .5rem;
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -929,14 +1093,33 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
#options-wrapper {
|
#options-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 0 1rem .5rem;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
#details-wrapper {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#options[open] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#sections-list[open] {
|
||||||
|
max-height: 102px;
|
||||||
|
}
|
||||||
|
#sections-list[open] #toc {
|
||||||
|
max-height: 60px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#header details:not(#options) {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
.options-column {
|
.options-column {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.options-column > .usercss-only {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
#options-wrapper .options-column:nth-child(2) {
|
#options-wrapper .options-column:nth-child(2) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -951,12 +1134,13 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
position: static;
|
position: static;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
#options summary {
|
#header summary {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
#options h2 {
|
#header summary h2 {
|
||||||
margin: 0 0 .5em;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.option label {
|
.option label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -970,15 +1154,12 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
top: 0.2rem;
|
top: 0.2rem;
|
||||||
}
|
}
|
||||||
#lint > .lint-scroll-container {
|
#lint > .lint-scroll-container {
|
||||||
margin: 32px 1rem 0;
|
padding-top: 0;
|
||||||
bottom: 6px;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
#lint {
|
#lint {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 1rem 0 0;
|
margin: .5rem 0 0;
|
||||||
}
|
|
||||||
#lint > summary {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
#lint:not([open]) + #footer {
|
#lint:not([open]) + #footer {
|
||||||
margin: .25em 0 -1em .25em;
|
margin: .25em 0 -1em .25em;
|
||||||
|
|
@ -994,8 +1175,9 @@ body.linter-disabled .hidden-unless-compact {
|
||||||
margin: 0 .5rem;
|
margin: 0 .5rem;
|
||||||
padding: .5rem 0;
|
padding: .5rem 0;
|
||||||
}
|
}
|
||||||
.usercss .CodeMirror-scroll {
|
.single-editor {
|
||||||
max-height: calc(100vh - var(--header-narrow-min-height));
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.usercss #options:not([open]) ~ #lint.hidden ~ #footer,
|
.usercss #options:not([open]) ~ #lint.hidden ~ #footer,
|
||||||
.usercss #lint:not([open]) + #footer {
|
.usercss #lint:not([open]) + #footer {
|
||||||
|
|
|
||||||
859
edit/edit.js
859
edit/edit.js
|
|
@ -1,337 +1,118 @@
|
||||||
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
/* global $ $create messageBoxProxy waitForSheet */// dom.js
|
||||||
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
|
/* global API msg */// msg.js
|
||||||
closeCurrentTab messageBox debounce
|
/* global CodeMirror */
|
||||||
initBeautifyButton ignoreChromeError dirtyReporter linter
|
/* global SectionsEditor */
|
||||||
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
|
/* global SourceEditor */
|
||||||
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
/* global baseInit */
|
||||||
|
/* global clipString createHotkeyInput helpPopup */// util.js
|
||||||
|
/* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
|
||||||
|
/* global cmFactory */
|
||||||
|
/* global editor */
|
||||||
|
/* global linterMan */
|
||||||
|
/* global prefs */
|
||||||
|
/* global t */// localization.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let saveSizeOnClose;
|
//#region init
|
||||||
|
|
||||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
baseInit.ready.then(async () => {
|
||||||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
await waitForSheet();
|
||||||
const CssToProperty = Object.entries(propertyToCss)
|
(editor.isUsercss ? SourceEditor : SectionsEditor)();
|
||||||
.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);
|
|
||||||
|
|
||||||
lazyInit();
|
|
||||||
|
|
||||||
(async function init() {
|
|
||||||
const [style] = await Promise.all([
|
|
||||||
initStyleData(),
|
|
||||||
onDOMready(),
|
|
||||||
prefs.initializing.then(() => new Promise(resolve => {
|
|
||||||
const theme = prefs.get('editor.theme');
|
|
||||||
const el = $('#cm-theme');
|
|
||||||
if (theme === 'default') {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
// preload the theme so CodeMirror can use the correct metrics
|
|
||||||
el.href = `vendor/codemirror/theme/${theme}.css`;
|
|
||||||
el.addEventListener('load', resolve, {once: true});
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
]);
|
|
||||||
const usercss = isUsercss(style);
|
|
||||||
const dirty = dirtyReporter();
|
|
||||||
let wasDirty = false;
|
|
||||||
let nameTarget;
|
|
||||||
|
|
||||||
prefs.subscribe(['editor.linter'], updateLinter);
|
|
||||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
|
||||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
|
||||||
showHotkeyInTooltip();
|
|
||||||
buildThemeElement();
|
|
||||||
buildKeymapElement();
|
|
||||||
setupLivePrefs();
|
|
||||||
initNameArea();
|
|
||||||
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
|
||||||
initResizeListener();
|
|
||||||
detectLayout();
|
|
||||||
updateTitle();
|
|
||||||
|
|
||||||
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
|
||||||
$('#preview-label').classList.toggle('hidden', !style.id);
|
|
||||||
|
|
||||||
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
|
||||||
style,
|
|
||||||
dirty,
|
|
||||||
updateName,
|
|
||||||
toggleStyle,
|
|
||||||
});
|
|
||||||
dirty.onChange(updateDirty);
|
|
||||||
await editor.ready;
|
await editor.ready;
|
||||||
|
editor.ready = true;
|
||||||
|
editor.dirty.onChange(editor.updateDirty);
|
||||||
|
|
||||||
|
prefs.subscribe('editor.linter', (key, value) => {
|
||||||
|
document.body.classList.toggle('linter-disabled', value === '');
|
||||||
|
linterMan.run();
|
||||||
|
});
|
||||||
|
|
||||||
// enabling after init to prevent flash of validation failure on an empty name
|
// enabling after init to prevent flash of validation failure on an empty name
|
||||||
$('#name').required = !usercss;
|
$('#name').required = !editor.isUsercss;
|
||||||
$('#save-button').onclick = editor.save;
|
$('#save-button').onclick = editor.save;
|
||||||
|
|
||||||
function initNameArea() {
|
const elSec = $('#sections-list');
|
||||||
const nameEl = $('#name');
|
// editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work
|
||||||
const resetEl = $('#reset-name');
|
if (elSec.open) editor.updateToc();
|
||||||
const isCustomName = style.updateUrl || usercss;
|
// and we also toggle `open` directly in other places e.g. in detectLayout()
|
||||||
nameTarget = isCustomName ? 'customName' : 'name';
|
new MutationObserver(() => elSec.open && editor.updateToc())
|
||||||
nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
.observe(elSec, {attributes: true, attributeFilter: ['open']});
|
||||||
nameEl.title = isCustomName ? t('customNameHint') : '';
|
|
||||||
nameEl.addEventListener('input', () => {
|
|
||||||
updateName(true);
|
|
||||||
resetEl.hidden = false;
|
|
||||||
});
|
|
||||||
resetEl.hidden = !style.customName;
|
|
||||||
resetEl.onclick = () => {
|
|
||||||
const style = editor.style;
|
|
||||||
nameEl.focus();
|
|
||||||
nameEl.select();
|
|
||||||
// trying to make it undoable via Ctrl-Z
|
|
||||||
if (!document.execCommand('insertText', false, style.name)) {
|
|
||||||
nameEl.value = style.name;
|
|
||||||
updateName(true);
|
|
||||||
}
|
|
||||||
style.customName = null; // to delete it from db
|
|
||||||
resetEl.hidden = true;
|
|
||||||
};
|
|
||||||
const enabledEl = $('#enabled');
|
|
||||||
enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findKeyForCommand(command, map) {
|
$('#toc').onclick = e =>
|
||||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
editor.jumpToEditor([...$('#toc').children].indexOf(e.target));
|
||||||
let key = Object.keys(map).find(k => map[k] === command);
|
$('#keyMap-help').onclick = () =>
|
||||||
if (key) {
|
require(['/edit/show-keymap-help'], () => showKeymapHelp()); /* global showKeymapHelp */
|
||||||
return key;
|
$('#linter-settings').onclick = () =>
|
||||||
}
|
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
|
||||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
$('#lint-help').onclick = () =>
|
||||||
key = ft && findKeyForCommand(command, ft);
|
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
|
||||||
if (key) {
|
require([
|
||||||
return key;
|
'/edit/autocomplete',
|
||||||
}
|
'/edit/global-search',
|
||||||
}
|
]);
|
||||||
return '';
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function buildThemeElement() {
|
//#endregion
|
||||||
CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme'));
|
//#region events
|
||||||
$('#editor.theme').append(...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 buildKeymapElement() {
|
const IGNORE_UPDATE_REASONS = [
|
||||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
'editPreview',
|
||||||
const maps = Object.keys(CodeMirror.keyMap)
|
'editPreviewEnd',
|
||||||
.map(name => ({
|
'editSave',
|
||||||
value: name,
|
'config',
|
||||||
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();
|
msg.onExtension(request => {
|
||||||
let bin = fragment;
|
const {style} = request;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initResizeListener() {
|
|
||||||
const {onBoundsChanged} = chrome.windows || {};
|
|
||||||
if (onBoundsChanged) {
|
|
||||||
// * movement is reported even if the window wasn't resized
|
|
||||||
// * fired just once when done so debounce is not needed
|
|
||||||
onBoundsChanged.addListener(wnd => {
|
|
||||||
// getting the current window id as it may change if the user attached/detached the tab
|
|
||||||
chrome.windows.getCurrent(ownWnd => {
|
|
||||||
if (wnd.id === ownWnd.id) rememberWindowSize();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
|
|
||||||
detectLayout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStyle() {
|
|
||||||
$('#enabled').checked = !style.enabled;
|
|
||||||
updateEnabledness(!style.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDirty() {
|
|
||||||
const isDirty = dirty.isDirty();
|
|
||||||
if (wasDirty !== isDirty) {
|
|
||||||
wasDirty = isDirty;
|
|
||||||
document.body.classList.toggle('dirty', isDirty);
|
|
||||||
$('#save-button').disabled = !isDirty;
|
|
||||||
}
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateEnabledness(enabled) {
|
|
||||||
dirty.modify('enabled', style.enabled, enabled);
|
|
||||||
style.enabled = enabled;
|
|
||||||
editor.updateLivePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateName(isUserInput) {
|
|
||||||
if (!editor) return;
|
|
||||||
if (isUserInput) {
|
|
||||||
const {value} = $('#name');
|
|
||||||
dirty.modify('name', style[nameTarget] || style.name, value);
|
|
||||||
style[nameTarget] = value;
|
|
||||||
}
|
|
||||||
updateTitle({});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTitle() {
|
|
||||||
document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLinter(key, value) {
|
|
||||||
$('body').classList.toggle('linter-disabled', value === '');
|
|
||||||
linter.run();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* Stuff not needed for the main init so we can let it run at its own tempo */
|
|
||||||
async function lazyInit() {
|
|
||||||
const ownTabId = (await getOwnTab()).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;
|
|
||||||
}
|
|
||||||
const tabs = await browser.tabs.query({currentWindow: true});
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
switch (request.method) {
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (
|
if (editor.style.id === style.id && !IGNORE_UPDATE_REASONS.includes(request.reason)) {
|
||||||
editor.style.id === request.style.id &&
|
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
|
||||||
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
|
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
|
||||||
.includes(request.reason)
|
|
||||||
) {
|
|
||||||
Promise.resolve(
|
|
||||||
request.codeIsUpdated === false ?
|
|
||||||
request.style : API.getStyle(request.style.id)
|
|
||||||
)
|
|
||||||
.then(newStyle => {
|
|
||||||
editor.replaceStyle(newStyle, request.codeIsUpdated);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
if (editor.style.id === request.style.id) {
|
if (editor.style.id === style.id) {
|
||||||
document.removeEventListener('visibilitychange', beforeUnload);
|
|
||||||
document.removeEventListener('beforeunload', beforeUnload);
|
|
||||||
closeCurrentTab();
|
closeCurrentTab();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'editDeleteText':
|
case 'editDeleteText':
|
||||||
document.execCommand('delete');
|
document.execCommand('delete');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
/**
|
window.on('beforeunload', e => {
|
||||||
* Invoked for 'visibilitychange' event by default.
|
let pos;
|
||||||
* Invoked for 'beforeunload' event when the style is modified and unsaved.
|
if (editor.isWindowed &&
|
||||||
* See https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid
|
document.visibilityState === 'visible' &&
|
||||||
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal.
|
prefs.get('openEditInWindow') &&
|
||||||
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved.
|
( // only if not maximized
|
||||||
*/
|
screenX > 0 || outerWidth < screen.availWidth ||
|
||||||
function beforeUnload(e) {
|
screenY > 0 || outerHeight < screen.availHeight ||
|
||||||
if (saveSizeOnClose) rememberWindowSize();
|
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({
|
||||||
|
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,
|
||||||
|
sel: cm.isClean() && [cm.doc.sel.ranges, cm.doc.sel.primIndex],
|
||||||
|
})),
|
||||||
|
});
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
// blurring triggers 'change' or 'input' event if needed
|
// blurring triggers 'change' or 'input' event if needed
|
||||||
|
|
@ -339,249 +120,259 @@ function beforeUnload(e) {
|
||||||
// refocus if unloading was canceled
|
// refocus if unloading was canceled
|
||||||
setTimeout(() => activeElement.focus());
|
setTimeout(() => activeElement.focus());
|
||||||
}
|
}
|
||||||
if (editor && editor.dirty.isDirty()) {
|
if (editor.dirty.isDirty()) {
|
||||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||||
e.returnValue = t('styleChangesNotSaved');
|
e.returnValue = t('styleChangesNotSaved');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
function isUsercss(style) {
|
//#endregion
|
||||||
return (
|
//#region editor methods
|
||||||
style.usercssData ||
|
|
||||||
!style.id && prefs.get('newStyleAsUsercss')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initStyleData() {
|
(() => {
|
||||||
const params = new URLSearchParams(location.search);
|
const toc = [];
|
||||||
const id = Number(params.get('id'));
|
const {dirty} = editor;
|
||||||
const createEmptyStyle = () => ({
|
let {style} = editor;
|
||||||
name: params.get('domain') ||
|
let wasDirty = false;
|
||||||
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() {
|
Object.defineProperties(editor, {
|
||||||
if (id) {
|
scrollInfo: {
|
||||||
return API.getStyle(id);
|
get: () => style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]) || {},
|
||||||
}
|
},
|
||||||
return Promise.resolve(createEmptyStyle());
|
style: {
|
||||||
}
|
get: () => style,
|
||||||
}
|
set: val => (style = val),
|
||||||
|
},
|
||||||
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.key === 'Escape' &&
|
|
||||||
!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'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keydown', showHelp.close, true);
|
/** @namespace Editor */
|
||||||
$('.dismiss', div).onclick = showHelp.close;
|
Object.assign(editor, {
|
||||||
|
|
||||||
// reset any inline styles
|
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
|
||||||
div.style = 'display: block';
|
if (si && si.sel) {
|
||||||
|
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
|
||||||
|
cm.operation(() => {
|
||||||
|
cm.setSelections(...si.sel, {scroll: false});
|
||||||
|
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
|
||||||
|
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showHelp.originalFocus = document.activeElement;
|
toggleStyle() {
|
||||||
return div;
|
$('#enabled').checked = !style.enabled;
|
||||||
}
|
editor.updateEnabledness(!style.enabled);
|
||||||
|
},
|
||||||
|
|
||||||
function showCodeMirrorPopup(title, html, options) {
|
updateDirty() {
|
||||||
const popup = showHelp(title, html);
|
const isDirty = dirty.isDirty();
|
||||||
popup.classList.add('big');
|
if (wasDirty !== isDirty) {
|
||||||
|
wasDirty = isDirty;
|
||||||
|
document.body.classList.toggle('dirty', isDirty);
|
||||||
|
$('#save-button').disabled = !isDirty;
|
||||||
|
}
|
||||||
|
editor.updateTitle();
|
||||||
|
},
|
||||||
|
|
||||||
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
|
updateEnabledness(enabled) {
|
||||||
mode: 'css',
|
dirty.modify('enabled', style.enabled, enabled);
|
||||||
lineNumbers: true,
|
style.enabled = enabled;
|
||||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
editor.updateLivePreview();
|
||||||
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';
|
updateName(isUserInput) {
|
||||||
popup.style.pointerEvents = 'auto';
|
if (!editor) return;
|
||||||
|
if (isUserInput) {
|
||||||
|
const {value} = $('#name');
|
||||||
|
dirty.modify('name', style[editor.nameTarget] || style.name, value);
|
||||||
|
style[editor.nameTarget] = value;
|
||||||
|
}
|
||||||
|
editor.updateTitle();
|
||||||
|
},
|
||||||
|
|
||||||
const onKeyDown = event => {
|
updateToc(added = editor.sections) {
|
||||||
if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
if (!toc.el) {
|
||||||
const search = $('#search-replace-dialog');
|
toc.el = $('#toc');
|
||||||
const area = search && search.contains(document.activeElement) ? search : popup;
|
toc.elDetails = toc.el.closest('details');
|
||||||
moveFocus(area, event.shiftKey ? -1 : 1);
|
}
|
||||||
event.preventDefault();
|
if (!toc.elDetails.open) return;
|
||||||
}
|
const {sections} = editor;
|
||||||
};
|
const first = sections.indexOf(added[0]);
|
||||||
window.addEventListener('keydown', onKeyDown, true);
|
const elFirst = toc.el.children[first];
|
||||||
|
if (first >= 0 && (!added.focus || !elFirst)) {
|
||||||
window.addEventListener('closeHelp', () => {
|
for (let el = elFirst, i = first; i < sections.length; i++) {
|
||||||
window.removeEventListener('keydown', onKeyDown, true);
|
const entry = sections[i].tocEntry;
|
||||||
document.documentElement.style.removeProperty('pointer-events');
|
if (!deepEqual(entry, toc[i])) {
|
||||||
rerouteHotkeys(true);
|
if (!el) el = toc.el.appendChild($create('li', {tabIndex: 0}));
|
||||||
cm = popup.codebox = null;
|
el.tabIndex = entry.removed ? -1 : 0;
|
||||||
}, {once: true});
|
toc[i] = Object.assign({}, entry);
|
||||||
|
const s = el.textContent = clipString(entry.label) || (
|
||||||
return popup;
|
entry.target == null
|
||||||
}
|
? t('appliesToEverything')
|
||||||
|
: clipString(entry.target) + (entry.numTargets > 1 ? ', ...' : ''));
|
||||||
function rememberWindowSize() {
|
if (s.length > 30) el.title = s;
|
||||||
if (
|
}
|
||||||
document.visibilityState === 'visible' &&
|
el = el.nextElementSibling;
|
||||||
prefs.get('openEditInWindow') &&
|
|
||||||
!isWindowMaximized()
|
|
||||||
) {
|
|
||||||
prefs.set('windowPosition', {
|
|
||||||
left: window.screenX,
|
|
||||||
top: window.screenY,
|
|
||||||
width: window.outerWidth,
|
|
||||||
height: window.outerHeight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, {passive: true});
|
|
||||||
}
|
|
||||||
} 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', '');
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region editor livePreview
|
||||||
|
|
||||||
|
editor.livePreview = (() => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePreviewer(data) {
|
||||||
|
const errorContainer = $('#preview-errors');
|
||||||
|
try {
|
||||||
|
port.postMessage(preprocess ? await preprocess(data) : data);
|
||||||
|
errorContainer.classList.add('hidden');
|
||||||
|
} catch (err) {
|
||||||
|
if (Array.isArray(err)) {
|
||||||
|
err = err.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}`;
|
||||||
|
}
|
||||||
|
errorContainer.classList.remove('hidden');
|
||||||
|
errorContainer.onclick = () => {
|
||||||
|
messageBoxProxy.alert(err.message || `${err}`, 'pre');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region colorpickerHelper
|
||||||
|
|
||||||
|
(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hotkey) {
|
||||||
|
extraKeys[hotkey] = 'colorpicker';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
options.classList.remove('ignore-pref');
|
if (defaults.extraKeys) {
|
||||||
lint.classList.remove('ignore-pref');
|
delete defaults.extraKeys[keyName];
|
||||||
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});
|
||||||
|
|
||||||
|
await baseInit.domReady;
|
||||||
|
|
||||||
|
$('#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'));
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
|
|
||||||
function isWindowMaximized() {
|
//#endregion
|
||||||
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,88 +1,140 @@
|
||||||
/* global importScripts workerUtil CSSLint require metaParser */
|
/* global createWorkerApi */// worker-util.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
importScripts('/js/worker-util.js');
|
(() => {
|
||||||
const {createAPI, loadScript} = workerUtil;
|
const hasCurlyBraceError = warning =>
|
||||||
|
warning.text === 'Unnecessary curly bracket (CssSyntaxError)';
|
||||||
|
let sugarssFallback;
|
||||||
|
|
||||||
createAPI({
|
/** @namespace EditorWorker */
|
||||||
csslint: (code, config) => {
|
createWorkerApi({
|
||||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
|
|
||||||
return CSSLint.verify(code, config).messages
|
async csslint(code, config) {
|
||||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
require(['/js/csslint/parserlib', '/js/csslint/csslint']); /* global CSSLint */
|
||||||
},
|
return CSSLint
|
||||||
stylelint: (code, config) => {
|
.verify(code, config).messages
|
||||||
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||||
return require('stylelint').lint({code, config});
|
},
|
||||||
},
|
|
||||||
metalint: code => {
|
getCssPropsValues() {
|
||||||
loadScript(
|
require(['/js/csslint/parserlib']); /* global parserlib */
|
||||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
const {
|
||||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
css: {Colors, GlobalKeywords, Properties},
|
||||||
'/js/meta-parser.js'
|
util: {describeProp},
|
||||||
);
|
} = parserlib;
|
||||||
const result = metaParser.lint(code);
|
const namedColors = Object.keys(Colors);
|
||||||
// extract needed info
|
const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g;
|
||||||
result.errors = result.errors.map(err =>
|
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)) {
|
||||||
|
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 {own: res, global: GlobalKeywords};
|
||||||
|
},
|
||||||
|
|
||||||
|
getRules(linter) {
|
||||||
|
return ruleRetriever[linter](); // eslint-disable-line no-use-before-define
|
||||||
|
},
|
||||||
|
|
||||||
|
metalint(code) {
|
||||||
|
require(['/js/meta-parser']); /* global metaParser */
|
||||||
|
const result = metaParser.lint(code);
|
||||||
|
// extract needed info
|
||||||
|
result.errors = result.errors.map(err => ({
|
||||||
code: err.code,
|
code: err.code,
|
||||||
args: err.args,
|
args: err.args,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
index: err.index
|
index: err.index,
|
||||||
})
|
}));
|
||||||
);
|
return result;
|
||||||
return result;
|
},
|
||||||
},
|
|
||||||
getStylelintRules,
|
|
||||||
getCsslintRules
|
|
||||||
});
|
|
||||||
|
|
||||||
function getCsslintRules() {
|
async stylelint(opts) {
|
||||||
loadScript('/vendor-overwrites/csslint/csslint.js');
|
require(['/vendor/stylelint-bundle/stylelint-bundle.min']); /* global stylelint */
|
||||||
return CSSLint.getRules().map(rule => {
|
try {
|
||||||
const output = {};
|
let res;
|
||||||
for (const [key, value] of Object.entries(rule)) {
|
let pass = 0;
|
||||||
if (typeof value !== 'function') {
|
/* sugarss is used for stylus-lang by default,
|
||||||
output[key] = value;
|
but it fails on normal css syntax so we retry in css mode. */
|
||||||
|
const isSugarSS = opts.syntax === 'sugarss';
|
||||||
|
if (sugarssFallback && isSugarSS) opts.syntax = sugarssFallback;
|
||||||
|
while (
|
||||||
|
++pass <= 2 &&
|
||||||
|
(res = (await stylelint.lint(opts)).results[0]) &&
|
||||||
|
isSugarSS && res.warnings.some(hasCurlyBraceError)
|
||||||
|
) sugarssFallback = opts.syntax = 'css';
|
||||||
|
delete res._postcssResult; // huge and unused
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
delete e.postcssNode; // huge, unused, non-transferable
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
return output;
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function getStylelintRules() {
|
const ruleRetriever = {
|
||||||
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
|
||||||
const stylelint = require('stylelint');
|
csslint() {
|
||||||
const options = {};
|
require(['/js/csslint/csslint']);
|
||||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
return CSSLint.getRuleList().map(rule => {
|
||||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
const output = {};
|
||||||
for (const id of Object.keys(stylelint.rules)) {
|
for (const [key, value] of Object.entries(rule)) {
|
||||||
const ruleCode = String(stylelint.rules[id]);
|
if (typeof value !== 'function') {
|
||||||
const sets = [];
|
output[key] = value;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
if (possible.includes('ignoreAtRules')) {
|
return options;
|
||||||
set.push('ignoreAtRules');
|
},
|
||||||
}
|
};
|
||||||
if (possible.includes('ignoreShorthands')) {
|
})();
|
||||||
set.push('ignoreShorthands');
|
|
||||||
}
|
|
||||||
if (set.length) {
|
|
||||||
sets.push(set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sets.length) {
|
|
||||||
options[id] = sets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
114
edit/embedded-popup.js
Normal file
114
edit/embedded-popup.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/* global $ $create $remove getEventKeyName */// dom.js
|
||||||
|
/* global CodeMirror */
|
||||||
|
/* global baseInit */// base.js
|
||||||
|
/* 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,
|
||||||
|
});
|
||||||
|
document.documentElement.appendChild(btn);
|
||||||
|
baseInit.domReady.then(() => {
|
||||||
|
document.body.appendChild(btn);
|
||||||
|
// Adding a dummy command to show in keymap help popup
|
||||||
|
CodeMirror.defaults.extraKeys[POPUP_HOTKEY] = 'openStylusPopup';
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (pw.IntersectionObserver) {
|
||||||
|
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
|
||||||
|
$create('div', {style: {height: '1px', marginTop: '-1px'}})
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
frame.dataset.loaded = '';
|
||||||
|
frame.height = body.scrollHeight;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -183,7 +183,8 @@
|
||||||
|
|
||||||
/*********** CM search highlight restyling, which shouldn't need color variables ****************/
|
/*********** CM search highlight restyling, which shouldn't need color variables ****************/
|
||||||
body.find-open .search-target-editor {
|
body.find-open .search-target-editor {
|
||||||
outline-color: darkorange !important;
|
box-shadow: 0 0 0 1px hsl(33, 100%, 50%), 0 0 3px hsla(33, 100%, 50%, .4);
|
||||||
|
/* same as our global.css focus rule */
|
||||||
}
|
}
|
||||||
|
|
||||||
body.find-open .cm-searching {
|
body.find-open .cm-searching {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
/* global CodeMirror focusAccessibility colorMimicry editor chromeLocal
|
/* global $ $$ $create $remove focusAccessibility toggleDataset */// dom.js
|
||||||
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
|
/* global CodeMirror */
|
||||||
|
/* global chromeLocal */// storage-util.js
|
||||||
|
/* global colorMimicry */
|
||||||
|
/* global debounce stringAsRegExp tryRegExp */// toolbox.js
|
||||||
|
/* global editor */
|
||||||
|
/* global t */// localization.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
(() => {
|
||||||
|
require(['/edit/global-search.css']);
|
||||||
|
|
||||||
//region Constants and state
|
//region Constants and state
|
||||||
|
|
||||||
|
|
@ -10,7 +16,6 @@ onDOMready().then(() => {
|
||||||
const ANNOTATE_SCROLLBAR_DELAY = 350;
|
const ANNOTATE_SCROLLBAR_DELAY = 350;
|
||||||
const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3};
|
const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3};
|
||||||
const STORAGE_UPDATE_DELAY = 500;
|
const STORAGE_UPDATE_DELAY = 500;
|
||||||
const SCROLL_REVEAL_MIN_PX = 50;
|
|
||||||
|
|
||||||
const DIALOG_SELECTOR = '#search-replace-dialog';
|
const DIALOG_SELECTOR = '#search-replace-dialog';
|
||||||
const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style';
|
const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style';
|
||||||
|
|
@ -22,6 +27,7 @@ onDOMready().then(() => {
|
||||||
const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
|
const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
firstRun: true,
|
||||||
// used for case-sensitive matching directly
|
// used for case-sensitive matching directly
|
||||||
find: '',
|
find: '',
|
||||||
// used when /re/ is detected or for case-insensitive matching
|
// used when /re/ is detected or for case-insensitive matching
|
||||||
|
|
@ -64,10 +70,12 @@ onDOMready().then(() => {
|
||||||
if (found) {
|
if (found) {
|
||||||
const target = $('.' + TARGET_CLASS);
|
const target = $('.' + TARGET_CLASS);
|
||||||
const cm = target.CodeMirror;
|
const cm = target.CodeMirror;
|
||||||
(cm || target).focus();
|
/* Since this runs in `keydown` event we have to delay focusing
|
||||||
|
* to prevent CodeMirror from seeing and handling the key */
|
||||||
|
setTimeout(() => (cm || target).focus());
|
||||||
if (cm) {
|
if (cm) {
|
||||||
const pos = cm.state.search.searchPos;
|
const {from, to} = cm.state.search.searchPos;
|
||||||
cm.setSelection(pos.from, pos.to);
|
cm.jumpToPos(from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
destroyDialog({restoreFocus: !found});
|
destroyDialog({restoreFocus: !found});
|
||||||
|
|
@ -78,7 +86,7 @@ onDOMready().then(() => {
|
||||||
doReplace();
|
doReplace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return !event.target.closest(focusAccessibility.ELEMENTS.join(','));
|
return !focusAccessibility.closest(event.target);
|
||||||
},
|
},
|
||||||
'Esc': () => {
|
'Esc': () => {
|
||||||
destroyDialog({restoreFocus: true});
|
destroyDialog({restoreFocus: true});
|
||||||
|
|
@ -99,7 +107,7 @@ onDOMready().then(() => {
|
||||||
state.lastFind = '';
|
state.lastFind = '';
|
||||||
toggleDataset(this, 'enabled', !state.icase);
|
toggleDataset(this, 'enabled', !state.icase);
|
||||||
doSearch({canAdvance: false});
|
doSearch({canAdvance: false});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,17 +133,17 @@ onDOMready().then(() => {
|
||||||
},
|
},
|
||||||
onfocusout() {
|
onfocusout() {
|
||||||
if (!state.dialog.contains(document.activeElement)) {
|
if (!state.dialog.contains(document.activeElement)) {
|
||||||
state.dialog.addEventListener('focusin', EVENTS.onfocusin);
|
state.dialog.on('focusin', EVENTS.onfocusin);
|
||||||
state.dialog.removeEventListener('focusout', EVENTS.onfocusout);
|
state.dialog.off('focusout', EVENTS.onfocusout);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onfocusin() {
|
onfocusin() {
|
||||||
state.dialog.addEventListener('focusout', EVENTS.onfocusout);
|
state.dialog.on('focusout', EVENTS.onfocusout);
|
||||||
state.dialog.removeEventListener('focusin', EVENTS.onfocusin);
|
state.dialog.off('focusin', EVENTS.onfocusin);
|
||||||
trimUndoHistory();
|
trimUndoHistory();
|
||||||
enableUndoButton(state.undoHistory.length);
|
enableUndoButton(state.undoHistory.length);
|
||||||
if (state.find) doSearch({canAdvance: false});
|
if (state.find) doSearch({canAdvance: false});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DIALOG_PROPS = {
|
const DIALOG_PROPS = {
|
||||||
|
|
@ -151,7 +159,7 @@ onDOMready().then(() => {
|
||||||
state.replace = this.value;
|
state.replace = this.value;
|
||||||
adjustTextareaSize(this);
|
adjustTextareaSize(this);
|
||||||
debounce(writeStorage, STORAGE_UPDATE_DELAY);
|
debounce(writeStorage, STORAGE_UPDATE_DELAY);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -168,7 +176,7 @@ onDOMready().then(() => {
|
||||||
replace(cm) {
|
replace(cm) {
|
||||||
state.reverse = false;
|
state.reverse = false;
|
||||||
focusDialog('replace', cm);
|
focusDialog('replace', cm);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
COMMANDS.replaceAll = COMMANDS.replace;
|
COMMANDS.replaceAll = COMMANDS.replace;
|
||||||
|
|
||||||
|
|
@ -176,7 +184,6 @@ onDOMready().then(() => {
|
||||||
|
|
||||||
Object.assign(CodeMirror.commands, COMMANDS);
|
Object.assign(CodeMirror.commands, COMMANDS);
|
||||||
readStorage();
|
readStorage();
|
||||||
return;
|
|
||||||
|
|
||||||
//region Find
|
//region Find
|
||||||
|
|
||||||
|
|
@ -241,6 +248,7 @@ onDOMready().then(() => {
|
||||||
} else {
|
} else {
|
||||||
showTally(0, 0);
|
showTally(0, 0);
|
||||||
}
|
}
|
||||||
|
state.firstRun = false;
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -559,15 +567,16 @@ onDOMready().then(() => {
|
||||||
|
|
||||||
function createDialog(type) {
|
function createDialog(type) {
|
||||||
state.originalFocus = document.activeElement;
|
state.originalFocus = document.activeElement;
|
||||||
|
state.firstRun = true;
|
||||||
|
|
||||||
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true);
|
const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
|
||||||
Object.assign(dialog, DIALOG_PROPS.dialog);
|
Object.assign(dialog, DIALOG_PROPS.dialog);
|
||||||
dialog.addEventListener('focusout', EVENTS.onfocusout);
|
dialog.on('focusout', EVENTS.onfocusout);
|
||||||
dialog.dataset.type = type;
|
dialog.dataset.type = type;
|
||||||
dialog.style.pointerEvents = 'auto';
|
dialog.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
const content = $('[data-type="content"]', dialog);
|
const content = $('[data-type="content"]', dialog);
|
||||||
content.parentNode.replaceChild(template[type].cloneNode(true), content);
|
content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
|
||||||
|
|
||||||
createInput(0, 'input', state.find);
|
createInput(0, 'input', state.find);
|
||||||
createInput(1, 'input2', state.replace);
|
createInput(1, 'input2', state.replace);
|
||||||
|
|
@ -575,9 +584,9 @@ onDOMready().then(() => {
|
||||||
state.tally = $('[data-type="tally"]', dialog);
|
state.tally = $('[data-type="tally"]', dialog);
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
body: colorMimicry.get(document.body, {bg: 'backgroundColor'}),
|
body: colorMimicry(document.body, {bg: 'backgroundColor'}),
|
||||||
input: colorMimicry.get($('input:not(:disabled)'), {bg: 'backgroundColor'}),
|
input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
|
||||||
icon: colorMimicry.get($$('svg.info')[1], {fill: 'fill'}),
|
icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
|
||||||
};
|
};
|
||||||
document.documentElement.appendChild(
|
document.documentElement.appendChild(
|
||||||
$(DIALOG_STYLE_SELECTOR) ||
|
$(DIALOG_STYLE_SELECTOR) ||
|
||||||
|
|
@ -630,14 +639,14 @@ onDOMready().then(() => {
|
||||||
input.value = value;
|
input.value = value;
|
||||||
Object.assign(input, DIALOG_PROPS[name]);
|
Object.assign(input, DIALOG_PROPS[name]);
|
||||||
|
|
||||||
input.parentElement.appendChild(template.clearSearch.cloneNode(true));
|
input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
|
||||||
$('[data-action]', input.parentElement)._input = input;
|
$('[data-action]', input.parentElement)._input = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function destroyDialog({restoreFocus = false} = {}) {
|
function destroyDialog({restoreFocus = false} = {}) {
|
||||||
state.input = null;
|
state.input = null;
|
||||||
$.remove(DIALOG_SELECTOR);
|
$remove(DIALOG_SELECTOR);
|
||||||
debounce.unregister(doSearch);
|
debounce.unregister(doSearch);
|
||||||
makeTargetVisible(null);
|
makeTargetVisible(null);
|
||||||
if (restoreFocus) {
|
if (restoreFocus) {
|
||||||
|
|
@ -671,7 +680,7 @@ onDOMready().then(() => {
|
||||||
el.style.width = newWidth + 'px';
|
el.style.width = newWidth + 'px';
|
||||||
}
|
}
|
||||||
const numLines = el.value.split('\n').length;
|
const numLines = el.value.split('\n').length;
|
||||||
if (numLines !== parseInt(el.rows)) {
|
if (numLines !== Number(el.rows)) {
|
||||||
el.rows = numLines;
|
el.rows = numLines;
|
||||||
}
|
}
|
||||||
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
|
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
|
||||||
|
|
@ -766,25 +775,22 @@ onDOMready().then(() => {
|
||||||
|
|
||||||
// scrolls the editor to reveal the match
|
// scrolls the editor to reveal the match
|
||||||
function makeMatchVisible(cm, searchCursor) {
|
function makeMatchVisible(cm, searchCursor) {
|
||||||
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
const canFocus = !state.firstRun && (!state.dialog || !state.dialog.contains(document.activeElement));
|
||||||
state.cm = cm;
|
state.cm = cm;
|
||||||
|
|
||||||
// scroll within the editor
|
// scroll within the editor
|
||||||
|
const pos = searchCursor.pos;
|
||||||
Object.assign(getStateSafe(cm), {
|
Object.assign(getStateSafe(cm), {
|
||||||
cursorPos: {
|
cursorPos: {
|
||||||
from: cm.getCursor('from'),
|
from: cm.getCursor('from'),
|
||||||
to: cm.getCursor('to'),
|
to: cm.getCursor('to'),
|
||||||
},
|
},
|
||||||
searchPos: searchCursor.pos,
|
searchPos: pos,
|
||||||
unclosedOp: !cm.curOp,
|
unclosedOp: !cm.curOp,
|
||||||
});
|
});
|
||||||
if (!cm.curOp) cm.startOperation();
|
if (!cm.curOp) cm.startOperation();
|
||||||
if (canFocus) cm.setSelection(searchCursor.pos.from, searchCursor.pos.to);
|
if (!state.firstRun) {
|
||||||
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX);
|
cm.jumpToPos(pos.from, pos.to);
|
||||||
|
}
|
||||||
// scroll to the editor itself
|
|
||||||
editor.scrollToEditor(cm);
|
|
||||||
|
|
||||||
// focus or expose as the current search target
|
// focus or expose as the current search target
|
||||||
clearMarker();
|
clearMarker();
|
||||||
if (canFocus) {
|
if (canFocus) {
|
||||||
|
|
@ -793,7 +799,6 @@ onDOMready().then(() => {
|
||||||
} else {
|
} else {
|
||||||
makeTargetVisible(cm.display.wrapper);
|
makeTargetVisible(cm.display.wrapper);
|
||||||
// mark the match
|
// mark the match
|
||||||
const pos = searchCursor.pos;
|
|
||||||
state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, {
|
state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, {
|
||||||
className: MATCH_CLASS,
|
className: MATCH_CLASS,
|
||||||
clearOnEnter: true,
|
clearOnEnter: true,
|
||||||
|
|
@ -871,15 +876,6 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toggleDataset(el, prop, state) {
|
|
||||||
if (state) {
|
|
||||||
el.dataset[prop] = '';
|
|
||||||
} else {
|
|
||||||
delete el.dataset[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function saveWindowScrollPos() {
|
function saveWindowScrollPos() {
|
||||||
state.scrollX = window.scrollX;
|
state.scrollX = window.scrollX;
|
||||||
state.scrollY = window.scrollY;
|
state.scrollY = window.scrollY;
|
||||||
|
|
@ -900,7 +896,9 @@ onDOMready().then(() => {
|
||||||
|
|
||||||
// produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
|
// produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
|
||||||
function radiateArray(arr, focalIndex) {
|
function radiateArray(arr, focalIndex) {
|
||||||
const result = [arr[focalIndex]];
|
const focus = arr[focalIndex];
|
||||||
|
if (!focus) return arr;
|
||||||
|
const result = [focus];
|
||||||
const len = arr.length;
|
const len = arr.length;
|
||||||
for (let i = 1; i < len; i++) {
|
for (let i = 1; i < len; i++) {
|
||||||
if (focalIndex + i < len) {
|
if (focalIndex + i < len) {
|
||||||
|
|
@ -946,4 +944,4 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
});
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox
|
|
||||||
LINTER_DEFAULTS rerouteHotkeys $ $create $createLink tryJSONparse t
|
|
||||||
chromeSync */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
$('#linter-settings').addEventListener('click', showLintConfig);
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
function stringifyConfig(config) {
|
|
||||||
return JSON.stringify(config, null, 2)
|
|
||||||
.replace(/,\n\s+\{\n\s+("severity":\s"\w+")\n\s+\}/g, ', {$1}');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLinterErrorMessage(title, contents, popup) {
|
|
||||||
messageBox({
|
|
||||||
title,
|
|
||||||
contents,
|
|
||||||
className: 'danger center lint-config',
|
|
||||||
buttons: [t('confirmOK')],
|
|
||||||
}).then(() => popup && popup.codebox && popup.codebox.focus());
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLintConfig() {
|
|
||||||
const linter = $('#editor.linter').value;
|
|
||||||
if (!linter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const storageName = linter === 'stylelint' ? 'editorStylelintConfig' : 'editorCSSLintConfig';
|
|
||||||
const getRules = memoize(linter === 'stylelint' ?
|
|
||||||
editorWorker.getStylelintRules : editorWorker.getCsslintRules);
|
|
||||||
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
|
||||||
const defaultConfig = stringifyConfig(
|
|
||||||
linter === 'stylelint' ? LINTER_DEFAULTS.STYLELINT : LINTER_DEFAULTS.CSSLINT
|
|
||||||
);
|
|
||||||
const title = t('linterConfigPopupTitle', linterTitle);
|
|
||||||
const popup = showCodeMirrorPopup(title, null, {
|
|
||||||
lint: false,
|
|
||||||
extraKeys: {'Ctrl-Enter': save},
|
|
||||||
hintOptions: {hint},
|
|
||||||
});
|
|
||||||
$('.contents', popup).appendChild(makeFooter());
|
|
||||||
|
|
||||||
let cm = popup.codebox;
|
|
||||||
cm.focus();
|
|
||||||
chromeSync.getLZValue(storageName).then(config => {
|
|
||||||
cm.setValue(config ? stringifyConfig(config) : defaultConfig);
|
|
||||||
cm.clearHistory();
|
|
||||||
cm.markClean();
|
|
||||||
updateButtonState();
|
|
||||||
});
|
|
||||||
cm.on('changes', updateButtonState);
|
|
||||||
|
|
||||||
rerouteHotkeys(false);
|
|
||||||
window.addEventListener('closeHelp', () => {
|
|
||||||
rerouteHotkeys(true);
|
|
||||||
cm = null;
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
loadScript([
|
|
||||||
'/vendor/codemirror/mode/javascript/javascript.js',
|
|
||||||
'/vendor/codemirror/addon/lint/json-lint.js',
|
|
||||||
'/vendor/jsonlint/jsonlint.js'
|
|
||||||
]).then(() => {
|
|
||||||
cm.setOption('mode', 'application/json');
|
|
||||||
cm.setOption('lint', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
function findInvalidRules(config, linter) {
|
|
||||||
return getRules()
|
|
||||||
.then(rules => {
|
|
||||||
if (linter === 'stylelint') {
|
|
||||||
return Object.keys(config.rules).filter(k => !config.rules.hasOwnProperty(k));
|
|
||||||
}
|
|
||||||
const ruleSet = new Set(rules.map(r => r.id));
|
|
||||||
return Object.keys(config).filter(k => !ruleSet.has(k));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeFooter() {
|
|
||||||
return $create('div', [
|
|
||||||
$create('p', [
|
|
||||||
$createLink(
|
|
||||||
linter === 'stylelint'
|
|
||||||
? 'https://stylelint.io/user-guide/rules/'
|
|
||||||
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
|
|
||||||
t('linterRulesLink')),
|
|
||||||
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '',
|
|
||||||
]),
|
|
||||||
$create('.buttons', [
|
|
||||||
$create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')),
|
|
||||||
$create('button.cancel', {onclick: cancel}, t('confirmClose')),
|
|
||||||
$create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function save(event) {
|
|
||||||
if (event instanceof Event) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
const json = tryJSONparse(cm.getValue());
|
|
||||||
if (!json) {
|
|
||||||
showLinterErrorMessage(linter, t('linterJSONError'), popup);
|
|
||||||
cm.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
findInvalidRules(json, linter).then(invalid => {
|
|
||||||
if (invalid.length) {
|
|
||||||
showLinterErrorMessage(linter, [
|
|
||||||
t('linterInvalidConfigError'),
|
|
||||||
$create('ul', invalid.map(name => $create('li', name))),
|
|
||||||
], popup);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chromeSync.setLZValue(storageName, json);
|
|
||||||
cm.markClean();
|
|
||||||
cm.focus();
|
|
||||||
updateButtonState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
cm.setValue(defaultConfig);
|
|
||||||
cm.focus();
|
|
||||||
updateButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
$('.dismiss').dispatchEvent(new Event('click'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateButtonState() {
|
|
||||||
$('.save', popup).disabled = cm.isClean();
|
|
||||||
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
|
|
||||||
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hint(cm) {
|
|
||||||
return getRules().then(rules => {
|
|
||||||
let ruleIds, options;
|
|
||||||
if (linter === 'stylelint') {
|
|
||||||
ruleIds = Object.keys(rules);
|
|
||||||
options = rules;
|
|
||||||
} else {
|
|
||||||
ruleIds = rules.map(r => r.id);
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
const cursor = cm.getCursor();
|
|
||||||
const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
|
|
||||||
const {line, ch} = cursor;
|
|
||||||
|
|
||||||
const quoted = string.startsWith('"');
|
|
||||||
const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
|
|
||||||
const depth = getLexicalDepth(lexical);
|
|
||||||
|
|
||||||
const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
|
|
||||||
let [, prevWord] = search.find(true) || [];
|
|
||||||
let words = [];
|
|
||||||
|
|
||||||
if (depth === 1 && linter === 'stylelint') {
|
|
||||||
words = quoted ? ['rules'] : [];
|
|
||||||
} else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
|
|
||||||
words = ruleIds;
|
|
||||||
} else if (depth === 2 || depth === 3 && lexical.type === ']') {
|
|
||||||
words = !quoted ? ['true', 'false', 'null'] :
|
|
||||||
ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
|
|
||||||
} else if (depth === 4 && prevWord === 'severity') {
|
|
||||||
words = ['error', 'warning'];
|
|
||||||
} else if (depth === 4) {
|
|
||||||
words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
|
|
||||||
} else if (depth === 5 && lexical.type === ']' && quoted) {
|
|
||||||
while (prevWord && !ruleIds.includes(prevWord)) {
|
|
||||||
prevWord = (search.find(true) || [])[1];
|
|
||||||
}
|
|
||||||
words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
list: words.filter(word => word.startsWith(leftPart)),
|
|
||||||
from: {line, ch: start + (quoted ? 1 : 0)},
|
|
||||||
to: {line, ch: string.endsWith('"') ? end - 1 : end},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLexicalDepth(lexicalState) {
|
|
||||||
let depth = 0;
|
|
||||||
while ((lexicalState = lexicalState.prev)) {
|
|
||||||
depth++;
|
|
||||||
}
|
|
||||||
return depth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
/* exported LINTER_DEFAULTS */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const LINTER_DEFAULTS = (() => {
|
|
||||||
const SEVERITY = {severity: 'warning'};
|
|
||||||
const STYLELINT = {
|
|
||||||
// 'sugarss' is a indent-based syntax like Sass or Stylus
|
|
||||||
// ref: https://github.com/postcss/postcss#syntaxes
|
|
||||||
// syntax: 'sugarss',
|
|
||||||
// ** recommended rules **
|
|
||||||
// ref: https://github.com/stylelint/stylelint-config-recommended/blob/master/index.js
|
|
||||||
rules: {
|
|
||||||
'at-rule-no-unknown': [true, {
|
|
||||||
'ignoreAtRules': ['extend', 'extends', 'css', 'block'],
|
|
||||||
'severity': 'warning'
|
|
||||||
}],
|
|
||||||
'block-no-empty': [true, SEVERITY],
|
|
||||||
'color-no-invalid-hex': [true, SEVERITY],
|
|
||||||
'declaration-block-no-duplicate-properties': [true, {
|
|
||||||
'ignore': ['consecutive-duplicates-with-different-values'],
|
|
||||||
'severity': 'warning'
|
|
||||||
}],
|
|
||||||
'declaration-block-no-shorthand-property-overrides': [true, SEVERITY],
|
|
||||||
'font-family-no-duplicate-names': [true, SEVERITY],
|
|
||||||
'function-calc-no-unspaced-operator': [true, SEVERITY],
|
|
||||||
'function-linear-gradient-no-nonstandard-direction': [true, SEVERITY],
|
|
||||||
'keyframe-declaration-no-important': [true, SEVERITY],
|
|
||||||
'media-feature-name-no-unknown': [true, SEVERITY],
|
|
||||||
/* recommended true */
|
|
||||||
'no-empty-source': false,
|
|
||||||
'no-extra-semicolons': [true, SEVERITY],
|
|
||||||
'no-invalid-double-slash-comments': [true, SEVERITY],
|
|
||||||
'property-no-unknown': [true, SEVERITY],
|
|
||||||
'selector-pseudo-class-no-unknown': [true, SEVERITY],
|
|
||||||
'selector-pseudo-element-no-unknown': [true, SEVERITY],
|
|
||||||
'selector-type-no-unknown': false, // for scss/less/stylus-lang
|
|
||||||
'string-no-newline': [true, SEVERITY],
|
|
||||||
'unit-no-unknown': [true, SEVERITY],
|
|
||||||
|
|
||||||
// ** non-essential rules
|
|
||||||
'comment-no-empty': false,
|
|
||||||
'declaration-block-no-redundant-longhand-properties': false,
|
|
||||||
'shorthand-property-no-redundant-values': false,
|
|
||||||
|
|
||||||
// ** stylistic rules **
|
|
||||||
/*
|
|
||||||
'at-rule-empty-line-before': [
|
|
||||||
'always',
|
|
||||||
{
|
|
||||||
'except': [
|
|
||||||
'blockless-after-same-name-blockless',
|
|
||||||
'first-nested'
|
|
||||||
],
|
|
||||||
'ignore': [
|
|
||||||
'after-comment'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'at-rule-name-case': 'lower',
|
|
||||||
'at-rule-name-space-after': 'always-single-line',
|
|
||||||
'at-rule-semicolon-newline-after': 'always',
|
|
||||||
'block-closing-brace-empty-line-before': 'never',
|
|
||||||
'block-closing-brace-newline-after': 'always',
|
|
||||||
'block-closing-brace-newline-before': 'always-multi-line',
|
|
||||||
'block-closing-brace-space-before': 'always-single-line',
|
|
||||||
'block-opening-brace-newline-after': 'always-multi-line',
|
|
||||||
'block-opening-brace-space-after': 'always-single-line',
|
|
||||||
'block-opening-brace-space-before': 'always',
|
|
||||||
'color-hex-case': 'lower',
|
|
||||||
'color-hex-length': 'short',
|
|
||||||
'comment-empty-line-before': [
|
|
||||||
'always',
|
|
||||||
{
|
|
||||||
'except': [
|
|
||||||
'first-nested'
|
|
||||||
],
|
|
||||||
'ignore': [
|
|
||||||
'stylelint-commands'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'comment-whitespace-inside': 'always',
|
|
||||||
'custom-property-empty-line-before': [
|
|
||||||
'always',
|
|
||||||
{
|
|
||||||
'except': [
|
|
||||||
'after-custom-property',
|
|
||||||
'first-nested'
|
|
||||||
],
|
|
||||||
'ignore': [
|
|
||||||
'after-comment',
|
|
||||||
'inside-single-line-block'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'declaration-bang-space-after': 'never',
|
|
||||||
'declaration-bang-space-before': 'always',
|
|
||||||
'declaration-block-semicolon-newline-after': 'always-multi-line',
|
|
||||||
'declaration-block-semicolon-space-after': 'always-single-line',
|
|
||||||
'declaration-block-semicolon-space-before': 'never',
|
|
||||||
'declaration-block-single-line-max-declarations': 1,
|
|
||||||
'declaration-block-trailing-semicolon': 'always',
|
|
||||||
'declaration-colon-newline-after': 'always-multi-line',
|
|
||||||
'declaration-colon-space-after': 'always-single-line',
|
|
||||||
'declaration-colon-space-before': 'never',
|
|
||||||
'declaration-empty-line-before': [
|
|
||||||
'always',
|
|
||||||
{
|
|
||||||
'except': [
|
|
||||||
'after-declaration',
|
|
||||||
'first-nested'
|
|
||||||
],
|
|
||||||
'ignore': [
|
|
||||||
'after-comment',
|
|
||||||
'inside-single-line-block'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'function-comma-newline-after': 'always-multi-line',
|
|
||||||
'function-comma-space-after': 'always-single-line',
|
|
||||||
'function-comma-space-before': 'never',
|
|
||||||
'function-max-empty-lines': 0,
|
|
||||||
'function-name-case': 'lower',
|
|
||||||
'function-parentheses-newline-inside': 'always-multi-line',
|
|
||||||
'function-parentheses-space-inside': 'never-single-line',
|
|
||||||
'function-whitespace-after': 'always',
|
|
||||||
'indentation': 2,
|
|
||||||
'length-zero-no-unit': true,
|
|
||||||
'max-empty-lines': 1,
|
|
||||||
'media-feature-colon-space-after': 'always',
|
|
||||||
'media-feature-colon-space-before': 'never',
|
|
||||||
'media-feature-name-case': 'lower',
|
|
||||||
'media-feature-parentheses-space-inside': 'never',
|
|
||||||
'media-feature-range-operator-space-after': 'always',
|
|
||||||
'media-feature-range-operator-space-before': 'always',
|
|
||||||
'media-query-list-comma-newline-after': 'always-multi-line',
|
|
||||||
'media-query-list-comma-space-after': 'always-single-line',
|
|
||||||
'media-query-list-comma-space-before': 'never',
|
|
||||||
'no-eol-whitespace': true,
|
|
||||||
'no-missing-end-of-source-newline': true,
|
|
||||||
'number-leading-zero': 'always',
|
|
||||||
'number-no-trailing-zeros': true,
|
|
||||||
'property-case': 'lower',
|
|
||||||
'rule-empty-line-before': [
|
|
||||||
'always-multi-line',
|
|
||||||
{
|
|
||||||
'except': [
|
|
||||||
'first-nested'
|
|
||||||
],
|
|
||||||
'ignore': [
|
|
||||||
'after-comment'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'selector-attribute-brackets-space-inside': 'never',
|
|
||||||
'selector-attribute-operator-space-after': 'never',
|
|
||||||
'selector-attribute-operator-space-before': 'never',
|
|
||||||
'selector-combinator-space-after': 'always',
|
|
||||||
'selector-combinator-space-before': 'always',
|
|
||||||
'selector-descendant-combinator-no-non-space': true,
|
|
||||||
'selector-list-comma-newline-after': 'always',
|
|
||||||
'selector-list-comma-space-before': 'never',
|
|
||||||
'selector-max-empty-lines': 0,
|
|
||||||
'selector-pseudo-class-case': 'lower',
|
|
||||||
'selector-pseudo-class-parentheses-space-inside': 'never',
|
|
||||||
'selector-pseudo-element-case': 'lower',
|
|
||||||
'selector-pseudo-element-colon-notation': 'double',
|
|
||||||
'selector-type-case': 'lower',
|
|
||||||
'unit-case': 'lower',
|
|
||||||
'value-list-comma-newline-after': 'always-multi-line',
|
|
||||||
'value-list-comma-space-after': 'always-single-line',
|
|
||||||
'value-list-comma-space-before': 'never',
|
|
||||||
'value-list-max-empty-lines': 0
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const CSSLINT = {
|
|
||||||
// Default warnings
|
|
||||||
'display-property-grouping': 1,
|
|
||||||
'duplicate-properties': 1,
|
|
||||||
'empty-rules': 1,
|
|
||||||
'errors': 1,
|
|
||||||
'warnings': 1,
|
|
||||||
'known-properties': 1,
|
|
||||||
|
|
||||||
// Default disabled
|
|
||||||
'adjoining-classes': 0,
|
|
||||||
'box-model': 0,
|
|
||||||
'box-sizing': 0,
|
|
||||||
'bulletproof-font-face': 0,
|
|
||||||
'compatible-vendor-prefixes': 0,
|
|
||||||
'duplicate-background-images': 0,
|
|
||||||
'fallback-colors': 0,
|
|
||||||
'floats': 0,
|
|
||||||
'font-faces': 0,
|
|
||||||
'font-sizes': 0,
|
|
||||||
'gradients': 0,
|
|
||||||
'ids': 0,
|
|
||||||
'import': 0,
|
|
||||||
'import-ie-limit': 0,
|
|
||||||
'important': 0,
|
|
||||||
'order-alphabetical': 0,
|
|
||||||
'outline-none': 0,
|
|
||||||
'overqualified-elements': 0,
|
|
||||||
'qualified-headings': 0,
|
|
||||||
'regex-selectors': 0,
|
|
||||||
'rules-count': 0,
|
|
||||||
'selector-max': 0,
|
|
||||||
'selector-max-approaching': 0,
|
|
||||||
'selector-newline': 0,
|
|
||||||
'shorthand': 0,
|
|
||||||
'star-property-hack': 0,
|
|
||||||
'text-indent': 0,
|
|
||||||
'underscore-property-hack': 0,
|
|
||||||
'unique-headings': 0,
|
|
||||||
'universal-selector': 0,
|
|
||||||
'unqualified-attributes': 0,
|
|
||||||
'vendor-prefix': 0,
|
|
||||||
'zero-units': 0
|
|
||||||
};
|
|
||||||
return {STYLELINT, CSSLINT, SEVERITY};
|
|
||||||
})();
|
|
||||||
248
edit/linter-dialogs.js
Normal file
248
edit/linter-dialogs.js
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
/* global $ $create $createLink messageBoxProxy */// dom.js
|
||||||
|
/* global chromeSync */// storage-util.js
|
||||||
|
/* global editor */
|
||||||
|
/* global helpPopup showCodeMirrorPopup */// util.js
|
||||||
|
/* global linterMan */
|
||||||
|
/* global t */// localization.js
|
||||||
|
/* global tryJSONparse */// toolbox.js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
/** @type {{csslint:{}, stylelint:{}}} */
|
||||||
|
const RULES = {};
|
||||||
|
let cm;
|
||||||
|
let defaultConfig;
|
||||||
|
let isStylelint;
|
||||||
|
let linter;
|
||||||
|
let popup;
|
||||||
|
|
||||||
|
linterMan.showLintConfig = async () => {
|
||||||
|
linter = await getLinter();
|
||||||
|
if (!linter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await require([
|
||||||
|
'/vendor/codemirror/mode/javascript/javascript',
|
||||||
|
'/vendor/codemirror/addon/lint/json-lint',
|
||||||
|
'/vendor/jsonlint/jsonlint',
|
||||||
|
]);
|
||||||
|
const config = await chromeSync.getLZValue(chromeSync.LZ_KEY[linter]);
|
||||||
|
const title = t('linterConfigPopupTitle', isStylelint ? 'Stylelint' : 'CSSLint');
|
||||||
|
isStylelint = linter === 'stylelint';
|
||||||
|
defaultConfig = stringifyConfig(linterMan.DEFAULTS[linter]);
|
||||||
|
popup = showCodeMirrorPopup(title, null, {
|
||||||
|
extraKeys: {'Ctrl-Enter': onConfigSave},
|
||||||
|
hintOptions: {hint},
|
||||||
|
lint: true,
|
||||||
|
mode: 'application/json',
|
||||||
|
value: config ? stringifyConfig(config) : defaultConfig,
|
||||||
|
});
|
||||||
|
$('.contents', popup).appendChild(
|
||||||
|
$create('div', [
|
||||||
|
$create('p', [
|
||||||
|
$createLink(
|
||||||
|
isStylelint
|
||||||
|
? 'https://stylelint.io/user-guide/rules/'
|
||||||
|
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
|
||||||
|
t('linterRulesLink')),
|
||||||
|
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '',
|
||||||
|
]),
|
||||||
|
$create('.buttons', [
|
||||||
|
$create('button.save', {onclick: onConfigSave, title: 'Ctrl-Enter'},
|
||||||
|
t('styleSaveLabel')),
|
||||||
|
$create('button.cancel', {onclick: onConfigCancel}, t('confirmClose')),
|
||||||
|
$create('button.reset', {onclick: onConfigReset, title: t('linterResetMessage')},
|
||||||
|
t('genericResetLabel')),
|
||||||
|
]),
|
||||||
|
]));
|
||||||
|
cm = popup.codebox;
|
||||||
|
cm.focus();
|
||||||
|
const rulesStr = getActiveRules().join('|');
|
||||||
|
if (rulesStr) {
|
||||||
|
const rx = new RegExp(`"(${rulesStr})"\\s*:`);
|
||||||
|
let line = 0;
|
||||||
|
cm.startOperation();
|
||||||
|
cm.eachLine(({text}) => {
|
||||||
|
const m = rx.exec(text);
|
||||||
|
if (m) {
|
||||||
|
const ch = m.index + 1;
|
||||||
|
cm.markText({line, ch}, {line, ch: ch + m[1].length}, {className: 'active-linter-rule'});
|
||||||
|
}
|
||||||
|
++line;
|
||||||
|
});
|
||||||
|
cm.endOperation();
|
||||||
|
}
|
||||||
|
cm.on('changes', updateConfigButtons);
|
||||||
|
updateConfigButtons();
|
||||||
|
window.on('closeHelp', onConfigClose, {once: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
linterMan.showLintHelp = async () => {
|
||||||
|
const linter = await getLinter();
|
||||||
|
const baseUrl = linter === 'stylelint'
|
||||||
|
? 'https://stylelint.io/user-guide/rules/'
|
||||||
|
: '';
|
||||||
|
let headerLink, template;
|
||||||
|
if (linter === 'csslint') {
|
||||||
|
headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules', 'CSSLint');
|
||||||
|
template = ruleID => {
|
||||||
|
const rule = RULES.csslint.find(rule => rule.id === ruleID);
|
||||||
|
return rule &&
|
||||||
|
$create('li', [
|
||||||
|
$create('b', ruleID + ': '),
|
||||||
|
rule.url ? $createLink(rule.url, rule.name) : $create('span', `"${rule.name}"`),
|
||||||
|
$create('p', rule.desc),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
headerLink = $createLink(baseUrl, 'stylelint');
|
||||||
|
template = rule =>
|
||||||
|
$create('li',
|
||||||
|
rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
|
||||||
|
}
|
||||||
|
const header = t('linterIssuesHelp', '\x01').split('\x01');
|
||||||
|
helpPopup.show(t('linterIssues'),
|
||||||
|
$create([
|
||||||
|
header[0], headerLink, header[1],
|
||||||
|
$create('ul.rules', getActiveRules().map(template)),
|
||||||
|
$create('button', {onclick: linterMan.showLintConfig}, t('configureStyle')),
|
||||||
|
]));
|
||||||
|
};
|
||||||
|
|
||||||
|
function getActiveRules() {
|
||||||
|
const all = [...linterMan.getIssues()].map(issue => issue.rule);
|
||||||
|
const uniq = new Set(all);
|
||||||
|
return [...uniq];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLexicalDepth(lexicalState) {
|
||||||
|
let depth = 0;
|
||||||
|
while ((lexicalState = lexicalState.prev)) {
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLinter() {
|
||||||
|
const val = $('#editor.linter').value;
|
||||||
|
if (val && !RULES[val]) {
|
||||||
|
RULES[val] = await linterMan.worker.getRules(val);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hint(cm) {
|
||||||
|
const rules = RULES[linter];
|
||||||
|
let ruleIds, options;
|
||||||
|
if (isStylelint) {
|
||||||
|
ruleIds = Object.keys(rules);
|
||||||
|
options = rules;
|
||||||
|
} else {
|
||||||
|
ruleIds = rules.map(r => r.id);
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
const cursor = cm.getCursor();
|
||||||
|
const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
|
||||||
|
const {line, ch} = cursor;
|
||||||
|
|
||||||
|
const quoted = string.startsWith('"');
|
||||||
|
const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
|
||||||
|
const depth = getLexicalDepth(lexical);
|
||||||
|
|
||||||
|
const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
|
||||||
|
let [, prevWord] = search.find(true) || [];
|
||||||
|
let words = [];
|
||||||
|
|
||||||
|
if (depth === 1 && isStylelint) {
|
||||||
|
words = quoted ? ['rules'] : [];
|
||||||
|
} else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
|
||||||
|
words = ruleIds;
|
||||||
|
} else if (depth === 2 || depth === 3 && lexical.type === ']') {
|
||||||
|
words = !quoted ? ['true', 'false', 'null'] :
|
||||||
|
ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
|
||||||
|
} else if (depth === 4 && prevWord === 'severity') {
|
||||||
|
words = ['error', 'warning'];
|
||||||
|
} else if (depth === 4) {
|
||||||
|
words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
|
||||||
|
} else if (depth === 5 && lexical.type === ']' && quoted) {
|
||||||
|
while (prevWord && !ruleIds.includes(prevWord)) {
|
||||||
|
prevWord = (search.find(true) || [])[1];
|
||||||
|
}
|
||||||
|
words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
list: words.filter(word => word.startsWith(leftPart)),
|
||||||
|
from: {line, ch: start + (quoted ? 1 : 0)},
|
||||||
|
to: {line, ch: string.endsWith('"') ? end - 1 : end},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConfigCancel() {
|
||||||
|
helpPopup.close();
|
||||||
|
editor.closestVisible().focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConfigClose() {
|
||||||
|
cm = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConfigReset(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
cm.setValue(defaultConfig);
|
||||||
|
cm.focus();
|
||||||
|
updateConfigButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfigSave(event) {
|
||||||
|
if (event instanceof Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
const json = tryJSONparse(cm.getValue());
|
||||||
|
if (!json) {
|
||||||
|
showLinterErrorMessage(linter, t('linterJSONError'), popup);
|
||||||
|
cm.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let invalid;
|
||||||
|
if (isStylelint) {
|
||||||
|
invalid = Object.keys(json.rules).filter(k => !RULES.stylelint.hasOwnProperty(k));
|
||||||
|
} else {
|
||||||
|
const ids = RULES.csslint.map(r => r.id);
|
||||||
|
invalid = Object.keys(json).filter(k => !ids.includes(k));
|
||||||
|
}
|
||||||
|
if (invalid.length) {
|
||||||
|
showLinterErrorMessage(linter, [
|
||||||
|
t('linterInvalidConfigError'),
|
||||||
|
$create('ul', invalid.map(name => $create('li', name))),
|
||||||
|
], popup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chromeSync.setLZValue(chromeSync.LZ_KEY[linter], json);
|
||||||
|
cm.markClean();
|
||||||
|
cm.focus();
|
||||||
|
updateConfigButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyConfig(config) {
|
||||||
|
return JSON.stringify(config, null, 2)
|
||||||
|
.replace(/,\n\s+{\n\s+("severity":\s"\w+")\n\s+}/g, ', {$1}');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showLinterErrorMessage(title, contents, popup) {
|
||||||
|
await messageBoxProxy.show({
|
||||||
|
title,
|
||||||
|
contents,
|
||||||
|
className: 'danger center lint-config',
|
||||||
|
buttons: [t('confirmOK')],
|
||||||
|
});
|
||||||
|
if (popup && popup.codebox) {
|
||||||
|
popup.codebox.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfigButtons() {
|
||||||
|
$('.save', popup).disabled = cm.isClean();
|
||||||
|
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
|
||||||
|
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
/* global LINTER_DEFAULTS linter editorWorker prefs chromeSync */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
registerLinters({
|
|
||||||
csslint: {
|
|
||||||
storageName: 'editorCSSLintConfig',
|
|
||||||
lint: csslint,
|
|
||||||
validMode: mode => mode === 'css',
|
|
||||||
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config)
|
|
||||||
},
|
|
||||||
stylelint: {
|
|
||||||
storageName: 'editorStylelintConfig',
|
|
||||||
lint: stylelint,
|
|
||||||
validMode: () => true,
|
|
||||||
getConfig: config => ({
|
|
||||||
syntax: 'sugarss',
|
|
||||||
rules: Object.assign({}, LINTER_DEFAULTS.STYLELINT.rules, config && config.rules)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function stylelint(text, config) {
|
|
||||||
return editorWorker.stylelint(text, config)
|
|
||||||
.then(({results}) => !results[0] ? [] :
|
|
||||||
results[0].warnings.map(({line, column: ch, text, severity}) => ({
|
|
||||||
from: {line: line - 1, ch: ch - 1},
|
|
||||||
to: {line: line - 1, ch},
|
|
||||||
message: text
|
|
||||||
.replace('Unexpected ', '')
|
|
||||||
.replace(/^./, firstLetter => firstLetter.toUpperCase())
|
|
||||||
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
|
|
||||||
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
|
|
||||||
severity,
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
function csslint(text, config) {
|
|
||||||
return editorWorker.csslint(text, config)
|
|
||||||
.then(results =>
|
|
||||||
results
|
|
||||||
.map(({line, col: ch, message, rule, type: severity}) => line && {
|
|
||||||
message,
|
|
||||||
from: {line: line - 1, ch: ch - 1},
|
|
||||||
to: {line: line - 1, ch},
|
|
||||||
rule: rule.id,
|
|
||||||
severity,
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerLinters(engines) {
|
|
||||||
const configs = new Map();
|
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener((changes, area) => {
|
|
||||||
if (area !== 'sync') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const [name, engine] of Object.entries(engines)) {
|
|
||||||
if (changes.hasOwnProperty(engine.storageName)) {
|
|
||||||
chromeSync.getLZValue(engine.storageName)
|
|
||||||
.then(config => {
|
|
||||||
configs.set(name, engine.getConfig(config));
|
|
||||||
linter.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
linter.register((text, options, cm) => {
|
|
||||||
const selectedLinter = prefs.get('editor.linter');
|
|
||||||
if (!selectedLinter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mode = cm.getOption('mode');
|
|
||||||
if (engines[selectedLinter].validMode(mode)) {
|
|
||||||
return runLint(selectedLinter);
|
|
||||||
}
|
|
||||||
for (const [name, engine] of Object.entries(engines)) {
|
|
||||||
if (engine.validMode(mode)) {
|
|
||||||
return runLint(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runLint(name) {
|
|
||||||
return getConfig(name)
|
|
||||||
.then(config => engines[name].lint(text, config, mode));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getConfig(name) {
|
|
||||||
if (configs.has(name)) {
|
|
||||||
return Promise.resolve(configs.get(name));
|
|
||||||
}
|
|
||||||
return chromeSync.getLZValue(engines[name].storageName)
|
|
||||||
.then(config => {
|
|
||||||
configs.set(name, engines[name].getConfig(config));
|
|
||||||
return configs.get(name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user