Compare commits

..

No commits in common. "master" and "v1.5.15" have entirely different histories.

284 changed files with 43856 additions and 33470 deletions

View File

@ -203,7 +203,7 @@ rules:
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
quote-props: [0]
quotes: [1, single, avoid-escape]
radix: [2, always]
radix: [2, as-needed]
require-jsdoc: [0]
require-yield: [2]
semi-spacing: [2, {before: false, after: true}]

View File

@ -24,9 +24,6 @@ If not, then provide details describing which page the feature will effect, e.g.
## Adding translations
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus).
Only the languages supported by the web store are allowed:
https://developer.chrome.com/docs/webstore/i18n/#localeTable
## Pull requests

9
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,9 @@
* **Browser**:
* **Operating System**:
* **Stylus Version**:
* **Screenshot**:
<!--
Please make sure you checked that your issue wasn't already addressed.
If the issue persists, please help us identifying the cause by providing the above details.
-->

View File

@ -1,48 +0,0 @@
---
name: Bug Report
about: Create a report about a bug you experienced while using Stylus.
title: "[Bug] Replace with title"
assignees: ''
---
<!--
⚠⚠ Do not delete this issue template! ⚠⚠
Reported issues must use this template and have all the necessary information provided.
Incomplete reports are likely to be ignored and closed.
-->
<!--
Thank you for taking the time to create a report about a bug.
Ensure that there are no other existing reports for this bug.
Please check if the issue is resolved after a restart of the browser.
Additionally, you should check if the issue persists in a new browser profile.
Remember to fill out every section on this report and remove any that are not needed.
Finally, place a brief description in the title of this report.
-->
# Bug Report
### Bug Description
<!-- Provide a clear and concise description, which will allow us to properly troubleshoot this bug. -->
### Screenshots
<!-- If applicable, add screenshots to help explain this bug. -->
### CSS Code
<!--
If the bug is related to (user)CSS or the editor,
please post the code (with a service like pastebin) in this bug report.
-->
### System Information
<!--
Specify the browser name and version as well as the Stylus version you are using.
Please do an online search for help if you are not familiar with how to get this information.
-->
- OS: <!-- e.g. Windows, macOS, Linux -->
- Browser: <!-- e.g. Chrome 91, Firefox 90, Edge 91, Safari 14 -->
- Stylus Version: <!-- e.g. 1.5.21 -->
### Additional Context
<!-- Provide any additional information about this bug. -->

View File

@ -7,7 +7,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- run: npm install

8
.gitignore vendored
View File

@ -1,8 +1,8 @@
*.zip
.DS_Store
.eslintcache
.transifexrc
pull_locales_login.rb
.vscode
desktop.ini
node_modules/
yarn.lock
*.zip
.eslintcache
.transifexrc

View File

@ -21,9 +21,12 @@ Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExt
## Screenshots
Manager | Editor | Popup search | Popup config | Manager config | Options
-|-|-|-|-|-
![Style manager](https://user-images.githubusercontent.com/1310400/34453460-214eaa5c-ed67-11e7-843b-d8960b71db6e.png) | ![Style editor](https://user-images.githubusercontent.com/1310400/34459585-3932cd94-ee05-11e7-9a1b-679522dddfb3.png) | ![Popup inline search](https://user-images.githubusercontent.com/1310400/34453463-21a44368-ed67-11e7-93b2-e1c8f5aac868.png) | ![Popup config for usercss](https://user-images.githubusercontent.com/1310400/34453462-218a589a-ed67-11e7-9040-7d0469eeadc3.png) | ![Style manager config for usercss](https://user-images.githubusercontent.com/1310400/34453464-21bdaf9c-ed67-11e7-8517-62d2f02e1918.png) | ![Options](https://user-images.githubusercontent.com/1310400/34453461-216aee4c-ed67-11e7-92db-ea21c1da5826.png)
![Style manager](https://user-images.githubusercontent.com/1310400/34453460-214eaa5c-ed67-11e7-843b-d8960b71db6e.png)
![Style editor](https://user-images.githubusercontent.com/1310400/34459585-3932cd94-ee05-11e7-9a1b-679522dddfb3.png)
![Popup config for usercss](https://user-images.githubusercontent.com/1310400/34453462-218a589a-ed67-11e7-9040-7d0469eeadc3.png)
![Popup inline search](https://user-images.githubusercontent.com/1310400/34453463-21a44368-ed67-11e7-93b2-e1c8f5aac868.png)
![Style manager config for usercss](https://user-images.githubusercontent.com/1310400/34453464-21bdaf9c-ed67-11e7-8517-62d2f02e1918.png)
![Options](https://user-images.githubusercontent.com/1310400/34453461-216aee4c-ed67-11e7-92db-ea21c1da5826.png)
## Help
@ -50,7 +53,7 @@ Copyright &copy; 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
Current Stylus:
Copyright &copy; 2017-2022 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
Copyright &copy; 2017-2019 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
**[GNU GPLv3](./LICENSE)**

View File

@ -1,18 +1,19 @@
{
"InaccessibleFileHint": {
"message": "Stylus لا يستطيع الوصول الى بعض انواع الملفات ( ملفات pdf و json )"
},
"addStyleLabel": {
"message": "كتابة نمط جديد"
"message": "كتابة نمط جديد",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "إضافة نمط"
"message": "إضافة نمط",
"description": "Title of the page for adding styles"
},
"appliesAdd": {
"message": "إضافة"
"message": "إضافة",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "ينطبق على: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -20,73 +21,84 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "و المزيد"
"message": "والمزيد",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "عناوين URL في النطاق"
"message": "عناوين URL في النطاق",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم."
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "ينطبق على"
"message": "ينطبق على",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesRegexpOption": {
"message": "عناوين URL التي تطابق regexp"
"message": "عناوين URL التي تطابق regexp",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "إزالة"
"message": "إزالة",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "تحديد"
"message": "تحديد",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "كل شيء"
"message": "كل شيء",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlOption": {
"message": "عنوان URL"
"message": "عنوان URL",
"description": "Option to make the style apply to the entered string as a URL"
},
"appliesUrlPrefixOption": {
"message": "عناوين URL البادئة بـ"
"message": "عناوين URL البادئة بـ",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"checkAllUpdates": {
"message": "البحث عن تحديثات لكل الأنماط"
"message": "البحث عن تحديثات لكل الأنماط",
"description": "Label for the button to check all styles for updates"
},
"checkForUpdate": {
"message": "البحث عن تحديث"
"message": "البحث عن تحديث",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "جارٍ البحث..."
},
"confirmDelete": {
"message": "حذف"
},
"confirmSave": {
"message": "حفظ"
"message": "جارٍ البحث...",
"description": "Text to display when checking a style for an update"
},
"deleteStyleConfirm": {
"message": "هل تريد بالتأكيد حذف هذا النمط؟"
"message": "هل تريد بالتأكيد حذف هذا النمط؟",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "حذف"
"message": "حذف",
"description": "Label for the button to delete a style"
},
"description": {
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى."
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى.",
"description": "Extension description"
},
"disableStyleLabel": {
"message": "تعطيل"
},
"editDeleteText": {
"message": "حذف"
"message": "تعطيل",
"description": "Label for the button to disable a style"
},
"editStyleHeading": {
"message": "تعديل النمط"
"message": "تعديل النمط",
"description": "Title of the page for editing styles"
},
"editStyleLabel": {
"message": "تعديل"
"message": "تعديل",
"description": "Label for the button to go to the edit style page"
},
"editStyleTitle": {
"message": "تعديل النمط $stylename$",
"description": "Title of the page for editing styles",
"placeholders": {
"stylename": {
"content": "$1"
@ -94,55 +106,60 @@
}
},
"enableStyleLabel": {
"message": "تمكين"
"message": "تمكين",
"description": "Label for the button to enable a style"
},
"genericAdd": {
"message": "إضافة"
},
"genericEnabledLabel": {
"message": "ممكّن"
"findStylesForSite": {
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا",
"description": "Text for a link that gets a list of styles for the current site"
},
"helpAlt": {
"message": "مساعدة"
"message": "مساعدة",
"description": "Alternate text for help buttons"
},
"installUpdate": {
"message": "تثبيت التحديث"
"message": "تثبيت التحديث",
"description": "Label for the button to install an update for a single style"
},
"manageHeading": {
"message": "الأنماط المثبتة"
"message": "الأنماط المثبتة",
"description": "Heading for the manage page"
},
"noStylesForSite": {
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا."
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا.",
"description": "Text displayed when no styles are installed for the current site"
},
"openManage": {
"message": "إدارة الأنماط المثبتة"
},
"optionsSyncUrl": {
"message": "عنوان URL"
"message": "إدارة الأنماط المثبتة",
"description": "Link to open the manage page."
},
"sectionAdd": {
"message": "إضافة قسم آخر"
"message": "إضافة قسم آخر",
"description": "Label for the button to add a section"
},
"sectionCode": {
"message": "الرمز"
"message": "الرمز",
"description": "Label for the code for a section"
},
"sectionRemove": {
"message": "إزالة القسم"
},
"sections": {
"message": "الأقسام"
"message": "إزالة القسم",
"description": "Label for the button to remove a section"
},
"styleCancelEditLabel": {
"message": "رجوع للإدارة"
"message": "رجوع للإدارة",
"description": "Label for cancel button for style editing"
},
"styleChangesNotSaved": {
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها."
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها.",
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
},
"styleEnabledLabel": {
"message": "ممكّن"
"message": "ممكّن",
"description": "Label for the enabled state of styles"
},
"styleInstall": {
"message": "هل تريد تثبيت '$stylename$' في Stylus؟",
"description": "Confirmation when installing a style",
"placeholders": {
"stylename": {
"content": "$1"
@ -150,16 +167,20 @@
}
},
"styleMissingName": {
"message": "أدخل اسمًا"
"message": "أدخل اسمًا",
"description": "Error displayed when user saves without providing a name"
},
"styleSaveLabel": {
"message": "حفظ"
"message": "حفظ",
"description": "Label for save button for style editing"
},
"styleToMozillaFormatHelp": {
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org."
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org.",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
},
"updateCheckFailBadResponseCode": {
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": {
"code": {
"content": "$1"
@ -167,12 +188,15 @@
}
},
"updateCheckFailServerUnreachable": {
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه."
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه.",
"description": "Text that displays when an update check failed because the update server is unreachable"
},
"updateCheckSucceededNoUpdate": {
"message": "النمط محدّث."
"message": "النمط محدّث.",
"description": "Text that displays when an update check completed and no update is available"
},
"updateCompleted": {
"message": "اكتمل التحديث."
"message": "اكتمل التحديث.",
"description": "Text that displays when an update completed"
}
}
}

View File

@ -1,15 +1,19 @@
{
"addStyleLabel": {
"message": "Писане на нов стил"
"message": "Писане на нов стил",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Добавяне на стил"
"message": "Добавяне на стил",
"description": "Title of the page for adding styles"
},
"appliesAdd": {
"message": "Добавяне"
"message": "Добавяне",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Приложимо за: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -17,148 +21,196 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "и още"
"message": "и още",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "Адреси на домейна"
"message": "Адреси на домейна",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела."
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "Приложимо за"
"message": "Приложимо за",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesRegexpOption": {
"message": "Адреси, съвпадащи с регулярен израз"
"message": "Адреси, съвпадащи с регулярен израз",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Премахване"
"message": "Премахване",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "Уточняване"
"message": "Уточняване",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Всичко"
"message": "Всичко",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlOption": {
"message": "Адрес"
"message": "Адрес",
"description": "Option to make the style apply to the entered string as a URL"
},
"appliesUrlPrefixOption": {
"message": "Адреси, започващи с"
"message": "Адреси, започващи с",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"applyAllUpdates": {
"message": "Прилагане на всички обновления"
"message": "Прилагане на всички обновления",
"description": "Label for the button to apply all detected updates"
},
"backupButtons": {
"message": "Резервни копия"
"message": "Резервни копия",
"description": "Heading for backup"
},
"backupMessage": {
"message": "Изберете файл или го влачете до страницата.",
"description": "Message for backup"
},
"bckpInstStyles": {
"message": "Изнасяне на стилове"
"message": "Изнасяне на стилове",
"description": ""
},
"checkAllUpdates": {
"message": "Проверка на всички стилове за обновления"
"message": "Проверка на всички стилове за обновления",
"description": "Label for the button to check all styles for updates"
},
"checkAllUpdatesForce": {
"message": "Повторна проверка"
"message": "Повторна проверка",
"description": "Label for the button to apply all detected updates"
},
"checkForUpdate": {
"message": "Проверка за обновления"
"message": "Проверка за обновления",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "Проверяване..."
"message": "Проверяване...",
"description": "Text to display when checking a style for an update"
},
"cm_autocompleteOnTyping": {
"message": "Автоматично завършване при въвеждане"
"message": "Автоматично завършване при въвеждане",
"description": "Label for the checkbox in the style editor."
},
"cm_indentWithTabs": {
"message": "Подпрозорци с умен отстъп"
"message": "Подпрозорци с умен отстъп",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
},
"cm_keyMap": {
"message": "Клавиши"
"message": "Клавиши",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"cm_lineWrapping": {
"message": "Пренасяне"
"message": "Пренасяне",
"description": "Label for the checkbox controlling word wrap option for the style editor."
},
"cm_matchHighlight": {
"message": "Осветяване"
"message": "Осветяване",
"description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor."
},
"cm_matchHighlightSelection": {
"message": "Само избраното"
"message": "Само избраното",
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
},
"cm_matchHighlightToken": {
"message": "Низа под показалеца"
"message": "Низа под показалеца",
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of the word/token under cursor even if nothing is selected"
},
"cm_resizeGripHint": {
"message": "Щракнете два пъти за възстановяване/увеличаване на височината"
"message": "Щракнете два пъти за възстановяване/увеличаване на височината",
"description": "Tooltip for the resize grip in style editor"
},
"cm_smartIndent": {
"message": "Умен отстъп"
"message": "Умен отстъп",
"description": "Label for the checkbox controlling smart indentation option for the style editor."
},
"cm_tabSize": {
"message": "Табулация"
"message": "Табулация",
"description": "Label for the text box controlling tab size option for the style editor."
},
"cm_theme": {
"message": "Тема"
"message": "Тема",
"description": "Label for the style editor's CSS theme."
},
"confirmCancel": {
"message": "Отказ"
"message": "Отказ",
"description": ""
},
"confirmDelete": {
"message": "Изтриване"
"message": "Изтриване",
"description": ""
},
"confirmNo": {
"message": "Не"
"message": "Не",
"description": "'No' button in a confirm dialog"
},
"confirmOK": {
"message": "Добре"
},
"confirmSave": {
"message": "Запазване"
"message": "Добре",
"description": ""
},
"confirmStop": {
"message": "Спиране"
"message": "Спиране",
"description": "'Stop' button in a confirm dialog"
},
"confirmYes": {
"message": "Да"
"message": "Да",
"description": "'Yes' button in a confirm dialog"
},
"dbError": {
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?"
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?",
"description": "Prompt when a DB error is encountered"
},
"defaultTheme": {
"message": "по подразбиране"
"message": "по подразбиране",
"description": "Default CodeMirror CSS theme option on the edit style page"
},
"deleteStyleConfirm": {
"message": "Сигурни ли сте, че искате да изтриете стила?"
"message": "Сигурни ли сте, че искате да изтриете стила?",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "Изтриване"
"message": "Изтриване",
"description": "Label for the button to delete a style"
},
"description": {
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове."
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове.",
"description": "Extension description"
},
"disableAllStyles": {
"message": "Изключване на всички стилове"
"message": "Изключване на всички стилове",
"description": "Label for the checkbox that turns all enabled styles off."
},
"disableStyleLabel": {
"message": "Изключване"
"message": "Изключване",
"description": "Label for the button to disable a style"
},
"dragDropMessage": {
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете."
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете.",
"description": "Drag'n'drop message"
},
"editDeleteText": {
"message": "Изтриване"
"message": "Изтриване",
"description": "Label for the context menu item in the editor to delete selected text"
},
"editGotoLine": {
"message": "Отиване на ред"
"message": "Отиване на ред",
"description": "Go to line or line:column on Ctrl-G in style code editor"
},
"editStyleHeading": {
"message": "Редактиране на стила"
"message": "Редактиране на стила",
"description": "Title of the page for editing styles"
},
"editStyleLabel": {
"message": "Редактиране"
"message": "Редактиране",
"description": "Label for the button to go to the edit style page"
},
"editStyleTitle": {
"message": "Редактиране на стила $stylename$",
"description": "Title of the page for editing styles",
"placeholders": {
"stylename": {
"content": "$1"
@ -166,91 +218,116 @@
}
},
"enableStyleLabel": {
"message": "Включване"
"message": "Включване",
"description": "Label for the button to enable a style"
},
"exportLabel": {
"message": "Изнасяне"
"message": "Изнасяне",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"genericAdd": {
"message": "Добавяне"
"findStylesForSite": {
"message": "Още стилове за този сайт",
"description": "Text for a link that gets a list of styles for the current site"
},
"genericDisabledLabel": {
"message": "Изключено"
},
"genericEnabledLabel": {
"message": "Включено"
"message": "Изключено",
"description": "Used in various lists/options to indicate that something is disabled"
},
"genericHistoryLabel": {
"message": "Хронология"
"message": "Хронология",
"description": "Used in various places to show a history log of something"
},
"helpAlt": {
"message": "Помощ"
"message": "Помощ",
"description": "Alternate text for help buttons"
},
"helpKeyMapCommand": {
"message": "Въведете име"
"message": "Въведете име",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"helpKeyMapHotkey": {
"message": "Натиснете клавиш"
"message": "Натиснете клавиш",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"importAppendLabel": {
"message": "Прибавяне към стила"
"message": "Прибавяне към стила",
"description": "Label for the button to import a style and append to the existing sections"
},
"importAppendTooltip": {
"message": "Прибавяне на внесения стил към текущия"
"message": "Прибавяне на внесения стил към текущия",
"description": "Tooltip for the button to import a style and append to the existing sections"
},
"importLabel": {
"message": "Внасяне"
"message": "Внасяне",
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
},
"importReplaceLabel": {
"message": "Презаписване на стила"
"message": "Презаписване на стила",
"description": "Label for the button to import and overwrite current style"
},
"importReplaceTooltip": {
"message": "Презаписване на съдържанието на текщия стил с това от внесения"
"message": "Презаписване на съдържанието на текщия стил с това от внесения",
"description": "Label for the button to import and overwrite current style"
},
"importReportLegendAdded": {
"message": "добавени"
"message": "добавени",
"description": "Text after the number of styles added in the report shown after importing styles"
},
"importReportLegendIdentical": {
"message": "пропуснати еднакви"
"message": "пропуснати еднакви",
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
},
"importReportLegendInvalid": {
"message": "пропуснати невалидни"
"message": "пропуснати невалидни",
"description": "Text after the number of styles skipped due to being invalid (not a Stylus/Stylish backup file probably) in the report shown after importing styles"
},
"importReportLegendUpdatedBoth": {
"message": "с обновени код и метаданни"
"message": "с обновени код и метаданни",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
},
"importReportLegendUpdatedCode": {
"message": "с обновен код"
"message": "с обновен код",
"description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles"
},
"importReportLegendUpdatedMeta": {
"message": "с обновени метаданни"
"message": "с обновени метаданни",
"description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles"
},
"importReportTitle": {
"message": "Внасянето на стилове завърши"
"message": "Внасянето на стилове завърши",
"description": "Title of the report shown after importing styles"
},
"importReportUnchanged": {
"message": "Нищо не беше променено."
"message": "Нищо не беше променено.",
"description": "Message in the report shown after importing styles"
},
"importReportUndone": {
"message": "върнати стила"
"message": "върнати стила",
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
},
"importReportUndoneTitle": {
"message": "Внасянето беше отменено"
"message": "Внасянето беше отменено",
"description": "Title of the message box shown after undoing the import of styles"
},
"installUpdate": {
"message": "Инсталиране на обновлението"
"message": "Инсталиране на обновлението",
"description": "Label for the button to install an update for a single style"
},
"linkGetHelp": {
"message": "Помощ"
"message": "Помощ",
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
},
"linkGetStyles": {
"message": "Вземете стилове"
"message": "Вземете стилове",
"description": "Help link text on the manage page e.g. https://userstyles.org"
},
"linterIssues": {
"message": "Проблеми"
"message": "Проблеми",
"description": "Label for the CSS linter issues block on the style edit page"
},
"linterIssuesHelp": {
"message": "Проблеми, намерени от $link$ при следните правила:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": {
"link": {
"content": "$1"
@ -258,187 +335,240 @@
}
},
"manageFavicons": {
"message": "Иконки на приложимите сайтовете"
"message": "Иконки на приложимите сайтовете",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"manageFaviconsGray": {
"message": "Сиви"
"message": "Сиви",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"manageFaviconsHelp": {
"message": "Разширението използва външна услуга https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"manageFilters": {
"message": "Филтри"
"message": "Филтри",
"description": "Label for filters container"
},
"manageHeading": {
"message": "Инсталирани стилове"
"message": "Инсталирани стилове",
"description": "Heading for the manage page"
},
"manageMaxTargets": {
"message": "Брой на видимите приложими адреси"
"message": "Брой на видимите приложими адреси",
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"manageNewUI": {
"message": "Нов интерфейс за управление"
"message": "Нов интерфейс за управление",
"description": "Label for the checkbox that toggles the new UI on manage page"
},
"manageOnlyEnabled": {
"message": "Само включените стилове"
"message": "Само включените стилове",
"description": "Checkbox to show only enabled styles"
},
"manageOnlyLocal": {
"message": "Само местно създадените стилове"
"message": "Само местно създадените стилове",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"manageOnlyLocalTooltip": {
"message": "(стиловете, които не са инсталирани през userstyles.org)"
"message": "(стиловете, които не са инсталирани през userstyles.org)",
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
},
"manageOnlyUpdates": {
"message": "Само стилове с обновления или проблеми"
"message": "Само стилове с обновления или проблеми",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
},
"manageTitle": {
"message": "Стилове"
"message": "Стилове",
"description": "Title for the manage page"
},
"menuShowBadge": {
"message": "Брой на активните стилове"
"message": "Брой на активните стилове",
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
},
"noStylesForSite": {
"message": "Няма инсталирани стилове за сайта."
"message": "Няма инсталирани стилове за сайта.",
"description": "Text displayed when no styles are installed for the current site"
},
"openManage": {
"message": "Управление"
},
"openOptions": {
"message": "Настройки"
"message": "Управление",
"description": "Link to open the manage page."
},
"openStylesManager": {
"message": "Управление на стиловете"
"message": "Управление на стиловете",
"description": "Label for the style maanger opener in the browser action context menu."
},
"optionsActions": {
"message": "Действия"
"message": "Действия",
"description": ""
},
"optionsAdvanced": {
"message": "Разширени"
"message": "Разширени",
"description": ""
},
"optionsAdvancedContextDelete": {
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора"
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора",
"description": ""
},
"optionsAdvancedExposeIframes": {
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]"
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]",
"description": ""
},
"optionsBadgeDisabled": {
"message": "Цвят на фона, когато е изключено"
"message": "Цвят на фона, когато е изключено",
"description": ""
},
"optionsBadgeNormal": {
"message": "Цвят на фона"
"message": "Цвят на фона",
"description": ""
},
"optionsCheck": {
"message": "Обновяване на стиловете"
"message": "Обновяване на стиловете",
"description": ""
},
"optionsCheckUpdate": {
"message": "Проверка и инсталиране на наличните обновления"
"message": "Проверка и инсталиране на наличните обновления",
"description": ""
},
"optionsCustomizeBadge": {
"message": "Значка на иконката на лентата"
"message": "Значка на иконката на лентата",
"description": ""
},
"optionsCustomizeIcon": {
"message": "Иконка на лентата със сечива"
"message": "Иконка на лентата със сечива",
"description": ""
},
"optionsCustomizePopup": {
"message": "Падащ прозорец"
"message": "Падащ прозорец",
"description": ""
},
"optionsCustomizeUpdate": {
"message": "Обновления"
"message": "Обновления",
"description": ""
},
"optionsHeading": {
"message": "Настройки"
"message": "Настройки",
"description": "Heading for options section on manage page."
},
"optionsIconDark": {
"message": "Тъмни теми"
"message": "Тъмни теми",
"description": ""
},
"optionsIconLight": {
"message": "Светли теми"
"message": "Светли теми",
"description": ""
},
"optionsOpen": {
"message": "Отваряне"
"message": "Отваряне",
"description": ""
},
"optionsOpenManager": {
"message": "Управление на стиловете"
"message": "Управление на стиловете",
"description": ""
},
"optionsPopupWidth": {
"message": "Ширина на падащия прозорец (в пиксели)"
"message": "Ширина на падащия прозорец (в пиксели)",
"description": ""
},
"optionsReset": {
"message": "Зануляване на настройки на първоначалните стойности"
"message": "Зануляване на настройки на първоначалните стойности",
"description": ""
},
"optionsResetButton": {
"message": "Зануляване на настройките"
"message": "Зануляване на настройките",
"description": ""
},
"optionsSubheading": {
"message": "Още настройки"
},
"optionsSyncUrl": {
"message": "Адрес"
"message": "Още настройки",
"description": "Subheading for options section on manage page."
},
"optionsUpdateImportNote": {
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални."
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални.",
"description": ""
},
"popupStylesFirst": {
"message": "Стилове преди командите"
"message": "Стилове преди командите",
"description": "Label for the checkbox controlling section order in the popup."
},
"prefShowBadge": {
"message": "Брой на активните стилове за текущия сайт"
"message": "Брой на активните стилове за текущия сайт",
"description": "Label for the checkbox controlling toolbar badge text."
},
"replace": {
"message": "Заместване"
"message": "Заместване",
"description": "Label before the replace input field in the editor shown on Ctrl-H"
},
"replaceAll": {
"message": "Заместване на всички"
"message": "Заместване на всички",
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
},
"replaceWith": {
"message": "Заместване с"
"message": "Заместване с",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"retrieveBckp": {
"message": "Внасяне на стилове"
"message": "Внасяне на стилове",
"description": ""
},
"search": {
"message": "Търсене"
"message": "Търсене",
"description": "Label before the search input field in the editor shown on Ctrl-F"
},
"searchRegexp": {
"message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази"
"message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"sectionAdd": {
"message": "Добавяне на друг отдел"
"message": "Добавяне на друг отдел",
"description": "Label for the button to add a section"
},
"sectionCode": {
"message": "Код"
"message": "Код",
"description": "Label for the code for a section"
},
"sectionRemove": {
"message": "Премахване на отдела"
},
"sections": {
"message": "Отдели"
"message": "Премахване на отдела",
"description": "Label for the button to remove a section"
},
"shortcuts": {
"message": "Клавишни комбинации"
"message": "Клавишни комбинации",
"description": "Go to shortcut configuration"
},
"shortcutsNote": {
"message": "Задаване на клавишни комбинации"
"message": "Задаване на клавишни комбинации",
"description": ""
},
"styleBadRegexp": {
"message": "Регулярният израз не е правилен."
"message": "Регулярният израз не е правилен.",
"description": "Validation message for a bad regexp in a style"
},
"styleBeautify": {
"message": "Разкрасяване"
"message": "Разкрасяване",
"description": "Label for the CSS-beautifier button on the edit style page"
},
"styleBeautifyIndentConditional": {
"message": "Отстъп на @media, @supports"
"message": "Отстъп на @media, @supports",
"description": "CSS-beautifier option"
},
"styleCancelEditLabel": {
"message": "Назад към стиловете"
"message": "Назад към стиловете",
"description": "Label for cancel button for style editing"
},
"styleChangesNotSaved": {
"message": "Направили сте промени по стила без да ги запазите."
"message": "Направили сте промени по стила без да ги запазите.",
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
},
"styleEnabledLabel": {
"message": "Включено"
"message": "Включено",
"description": "Label for the enabled state of styles"
},
"styleFromMozillaFormatPrompt": {
"message": "Поставете кода във формат на Мозила"
"message": "Поставете кода във формат на Мозила",
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
},
"styleInstall": {
"message": "Да се инсталира ли '$stylename$'?",
"description": "Confirmation when installing a style",
"placeholders": {
"stylename": {
"content": "$1"
@ -446,49 +576,68 @@
}
},
"styleMissingName": {
"message": "Въведете име"
"message": "Въведете име",
"description": "Error displayed when user saves without providing a name"
},
"styleMozillaFormatHeading": {
"message": "Формат на Мозила"
"message": "Формат на Мозила",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
},
"styleNotAppliedRegexpProblemTooltip": {
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази"
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"styleRegexpInvalidExplanation": {
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани."
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани.",
"description": ""
},
"styleRegexpPartialExplanation": {
"message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект)."
"message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект).",
"description": ""
},
"styleRegexpProblemTooltip": {
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази"
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази",
"description": "Tooltip in the popup for styles that were applied only partially"
},
"styleRegexpTestButton": {
"message": "Тест на регулярния израз",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
},
"styleRegexpTestFull": {
"message": "Съвпадащи подпрозорци"
"message": "Съвпадащи подпрозорци",
"description": "RegExp test report: label for the fully matching expressions"
},
"styleRegexpTestInvalid": {
"message": "Неправилните регулярни изрази са пропуснати"
"message": "Неправилните регулярни изрази са пропуснати",
"description": "RegExp test report: label for the invalid expressions"
},
"styleRegexpTestNone": {
"message": "Няма съвпадащи подпрозорци"
"message": "Няма съвпадащи подпрозорци",
"description": "RegExp test report: label for expressions that didn't match any tabs"
},
"styleRegexpTestPartial": {
"message": "Не съвпада напълно, затова е пропуснато"
"message": "Не съвпада напълно, затова е пропуснато",
"description": "RegExp test report: label for the partially matching expressions"
},
"styleRegexpTestTitle": {
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)"
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)",
"description": "RegExp test report: title of the report"
},
"styleSaveLabel": {
"message": "Запазване"
"message": "Запазване",
"description": "Label for save button for style editing"
},
"styleToMozillaFormatHelp": {
"message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)"
"message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
},
"styleToMozillaFormatTitle": {
"message": "Стил във формат на Мозила"
"message": "Стил във формат на Мозила",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"styleUpdate": {
"message": "Сигурни ли сте, че искате да обновите '$stylename$'?",
"description": "Confirmation when updating a style",
"placeholders": {
"stylename": {
"content": "$1"
@ -496,34 +645,44 @@
}
},
"stylusUnavailableForURL": {
"message": "Разширението не работи на такива страници."
"message": "Разширението не работи на такива страници.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"stylusUnavailableForURLdetails": {
"message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен."
"message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен.",
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
},
"toggleStyle": {
"message": "Превключване на стила"
"message": "Превключване на стила",
"description": "Label for the checkbox to enable/disable a style"
},
"undo": {
"message": "Отмяна"
"message": "Отмяна",
"description": "Button label"
},
"undoGlobal": {
"message": "Отмяна във всички отдели"
"message": "Отмяна във всички отдели",
"description": "CSS-beautify global Undo button label"
},
"unreachableContentScript": {
"message": "Няма връзка със страницата. Презаредете подпрозореца."
"message": "Няма връзка със страницата. Презаредете подпрозореца.",
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
},
"unreachableFileHint": {
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions."
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions.",
"description": "Note in the toolbar popup for file:// URLs"
},
"updateAllCheckSucceededNoUpdate": {
"message": "Няма намерени обновления."
"message": "Няма намерени обновления.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"updateAllCheckSucceededSomeEdited": {
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани)."
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани).",
"description": "Text that displays when an update all check completed and no updates are available"
},
"updateCheckFailBadResponseCode": {
"message": "Неуспешно обновяване: сървърът отговори с код $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": {
"code": {
"content": "$1"
@ -531,36 +690,47 @@
}
},
"updateCheckFailServerUnreachable": {
"message": "Неуспешно обновяване: няма връзка със сървъра."
"message": "Неуспешно обновяване: няма връзка със сървъра.",
"description": "Text that displays when an update check failed because the update server is unreachable"
},
"updateCheckHistory": {
"message": "Хронология на проверките"
"message": "Хронология на проверките",
"description": ""
},
"updateCheckManualUpdateForce": {
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)"
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"updateCheckManualUpdateHint": {
"message": "Принудителното обновяване ще презапише местните редакции."
"message": "Принудителното обновяване ще презапише местните редакции.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"updateCheckSkippedLocallyEdited": {
"message": "Стилът е бил местно редактиран."
"message": "Стилът е бил местно редактиран.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Стилът може да е бил местно редактиран."
"message": "Стилът може да е бил местно редактиран.",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
},
"updateCheckSucceededNoUpdate": {
"message": "Стилът е обновен."
"message": "Стилът е обновен.",
"description": "Text that displays when an update check completed and no update is available"
},
"updateCompleted": {
"message": "Обновяването е завършено."
"message": "Обновяването е завършено.",
"description": "Text that displays when an update completed"
},
"updatesCurrentlyInstalled": {
"message": "Инсталирани обновления:"
"message": "Инсталирани обновления:",
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
},
"writeStyleFor": {
"message": "Писане на стил за: "
"message": "Писане на стил за: ",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
},
"writeStyleForURL": {
"message": "този адрес"
"message": "този адрес",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
}
}
}

View File

@ -1,15 +1,19 @@
{
"addStyleLabel": {
"message": "Напиши нов стил"
"message": "Напиши нов стил",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Добави стил"
"message": "Добави стил",
"description": "Title of the page for adding styles"
},
"appliesAdd": {
"message": "Добави"
"message": "Добави",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Прилага се към: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -17,103 +21,136 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "и още"
"message": "и още",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "URLи на домейна"
"message": "URLи на домейна",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция."
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "Прилага се към"
"message": "Прилага се към",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesRegexpOption": {
"message": "Адреси, съвпадащи с regexp"
"message": "Адреси, съвпадащи с regexp",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Премахни"
"message": "Премахни",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "Уточни"
"message": "Уточни",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Всички"
"message": "Всички",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlPrefixOption": {
"message": "URL започващи с"
"message": "URL започващи с",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"applyAllUpdates": {
"message": "Приложи всички промени"
"message": "Приложи всички промени",
"description": "Label for the button to apply all detected updates"
},
"checkAllUpdates": {
"message": "Провери всички стилове за обновления"
"message": "Провери всички стилове за обновления",
"description": "Label for the button to check all styles for updates"
},
"checkForUpdate": {
"message": "Провери за обновление"
"message": "Провери за обновление",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "Проверявам..."
"message": "Проверявам...",
"description": "Text to display when checking a style for an update"
},
"cm_indentWithTabs": {
"message": "Използвай табулация с умно отместване"
"message": "Използвай табулация с умно отместване",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
},
"cm_keyMap": {
"message": "Клавишни комбинации"
"message": "Клавишни комбинации",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"cm_lineWrapping": {
"message": "Автоматично пренасяне"
"message": "Автоматично пренасяне",
"description": "Label for the checkbox controlling word wrap option for the style editor."
},
"cm_smartIndent": {
"message": "Използвай умно отместване"
"message": "Използвай умно отместване",
"description": "Label for the checkbox controlling smart indentation option for the style editor."
},
"cm_tabSize": {
"message": "Размер на табулацията"
"message": "Размер на табулацията",
"description": "Label for the text box controlling tab size option for the style editor."
},
"cm_theme": {
"message": "Тема"
"message": "Тема",
"description": "Label for the style editor's CSS theme."
},
"confirmNo": {
"message": "Не"
"message": "Не",
"description": "'No' button in a confirm dialog"
},
"confirmStop": {
"message": "Спри"
"message": "Спри",
"description": "'Stop' button in a confirm dialog"
},
"confirmYes": {
"message": "Да"
"message": "Да",
"description": "'Yes' button in a confirm dialog"
},
"dbError": {
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?"
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?",
"description": "Prompt when a DB error is encountered"
},
"defaultTheme": {
"message": "по подразбиране"
"message": "по подразбиране",
"description": "Default CodeMirror CSS theme option on the edit style page"
},
"deleteStyleConfirm": {
"message": "Наистина ли искаш да изтриеш този стил?"
"message": "Наистина ли искаш да изтриеш този стил?",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "Изтрий"
"message": "Изтрий",
"description": "Label for the button to delete a style"
},
"description": {
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове."
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове.",
"description": "Extension description"
},
"disableAllStyles": {
"message": "Изключи всички стилове"
"message": "Изключи всички стилове",
"description": "Label for the checkbox that turns all enabled styles off."
},
"disableStyleLabel": {
"message": "Забрани"
"message": "Забрани",
"description": "Label for the button to disable a style"
},
"editGotoLine": {
"message": "Иди на ред (или ред:кол)"
"message": "Иди на ред (или ред:кол)",
"description": "Go to line or line:column on Ctrl-G in style code editor"
},
"editStyleHeading": {
"message": "Промени стила"
"message": "Промени стила",
"description": "Title of the page for editing styles"
},
"editStyleLabel": {
"message": "Редактирай"
"message": "Редактирай",
"description": "Label for the button to go to the edit style page"
},
"editStyleTitle": {
"message": "Редактирай стил $stylename$",
"description": "Title of the page for editing styles",
"placeholders": {
"stylename": {
"content": "$1"
@ -121,52 +158,68 @@
}
},
"enableStyleLabel": {
"message": "Разреши"
"message": "Разреши",
"description": "Label for the button to enable a style"
},
"exportLabel": {
"message": "Експорт"
"message": "Експорт",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"helpAlt": {
"message": "Помощ"
"message": "Помощ",
"description": "Alternate text for help buttons"
},
"helpKeyMapCommand": {
"message": "Напиши име на команда"
"message": "Напиши име на команда",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"helpKeyMapHotkey": {
"message": "Натисни клавишна комбинация"
"message": "Натисни клавишна комбинация",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"importAppendLabel": {
"message": "Добави към стил"
"message": "Добави към стил",
"description": "Label for the button to import a style and append to the existing sections"
},
"importAppendTooltip": {
"message": "Добави импортирания стил към текущия"
"message": "Добави импортирания стил към текущия",
"description": "Tooltip for the button to import a style and append to the existing sections"
},
"importLabel": {
"message": "Импорт"
"message": "Импорт",
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
},
"importReplaceLabel": {
"message": "Презапиши стила"
"message": "Презапиши стила",
"description": "Label for the button to import and overwrite current style"
},
"importReplaceTooltip": {
"message": "Презапишете съдържанието на текущия стил с импортирания"
"message": "Презапишете съдържанието на текущия стил с импортирания",
"description": "Label for the button to import and overwrite current style"
},
"installButton": {
"message": "Инсталирай стил"
"message": "Инсталирай стил",
"description": "Label for install button"
},
"installButtonInstalled": {
"message": "Стилът е инсталиран"
"message": "Стилът е инсталиран",
"description": "Text displayed when the style is successfully installed"
},
"installButtonReinstall": {
"message": "Преинсталирай стила"
"message": "Преинсталирай стила",
"description": "Label for reinstall button"
},
"installButtonUpdate": {
"message": "Обнови стила"
"message": "Обнови стила",
"description": "Label for update button"
},
"installUpdate": {
"message": "Инсталирай обновление"
"message": "Инсталирай обновление",
"description": "Label for the button to install an update for a single style"
},
"installUpdateFrom": {
"message": "В момента стилът се обновява от $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
@ -174,22 +227,28 @@
}
},
"installUpdateFromLabel": {
"message": "Провери за обновления"
"message": "Провери за обновления",
"description": "Label for the checkbox to save current URL for update check"
},
"license": {
"message": "Лиценз"
"message": "Лиценз",
"description": "Label for the license"
},
"linkGetHelp": {
"message": "Получете помощ"
"message": "Получете помощ",
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
},
"linkGetStyles": {
"message": "Вземете стилове"
"message": "Вземете стилове",
"description": "Help link text on the manage page e.g. https://userstyles.org"
},
"linkTranslate": {
"message": "Преведете"
"message": "Преведете",
"description": "Transifex link text on the manage page"
},
"linterCSSLintIncompatible": {
"message": "CSSLint не поддържа $preprocessorname$ preprocessor",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
@ -197,10 +256,12 @@
}
},
"linterCSSLintSettings": {
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)"
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)",
"description": "CSSLint rule config values"
},
"linterConfigPopupTitle": {
"message": "Настройте конфигурация за $linter$ правила",
"description": "Stylelint or CSSLint popup header",
"placeholders": {
"linter": {
"content": "$1"
@ -208,16 +269,20 @@
}
},
"linterConfigTooltip": {
"message": "Щракнете, за да конфигурирате този linter"
"message": "Щракнете, за да конфигурирате този linter",
"description": "Icon tooltip to indicate that it opens a popup with the selected linter configuration"
},
"linterInvalidConfigError": {
"message": "Не е записано заради тези неправилни настройки"
"message": "Не е записано заради тези неправилни настройки",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
},
"linterIssues": {
"message": "Проблеми"
"message": "Проблеми",
"description": "Label for the CSS linter issues block on the style edit page"
},
"linterIssuesHelp": {
"message": "Тези проблеми бяха намерени от $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": {
"link": {
"content": "$1"
@ -225,54 +290,71 @@
}
},
"linterJSONError": {
"message": "Невалиден JSON формат"
"message": "Невалиден JSON формат",
"description": "Setting linter config with invalid JSON"
},
"linterResetMessage": {
"message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец"
"message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец",
"description": "Reset button tooltip to inform user on how to undo an accidental reset"
},
"linterRulesLink": {
"message": "Вижте пълния списък с правила"
"message": "Вижте пълния списък с правила",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"liveReloadError": {
"message": "Получи се грешка докато наблюдавахме файла"
"message": "Получи се грешка докато наблюдавахме файла",
"description": "The label of live-reload error"
},
"liveReloadLabel": {
"message": "Преглед на живо"
"message": "Преглед на живо",
"description": "The label of live-reload feature"
},
"manageFilters": {
"message": "Филтри"
"message": "Филтри",
"description": "Label for filters container"
},
"manageHeading": {
"message": "Инсталирани стилове"
"message": "Инсталирани стилове",
"description": "Heading for the manage page"
},
"manageNewStyleAsUsercss": {
"message": "като Потребителскиcss"
"message": "като Потребителскиcss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
},
"manageNewUI": {
"message": "Нова подредба на UI"
"message": "Нова подредба на UI",
"description": "Label for the checkbox that toggles the new UI on manage page"
},
"manageOnlyDisabled": {
"message": "Само забранените стилове"
"message": "Само забранените стилове",
"description": "Checkbox to show only disabled styles"
},
"manageOnlyEnabled": {
"message": "Само разрешените стилове"
"message": "Само разрешените стилове",
"description": "Checkbox to show only enabled styles"
},
"manageOnlyExternal": {
"message": "Само външните стилове"
"message": "Само външните стилове",
"description": "Checkbox to show only externally installed styles i.e. updatable"
},
"manageOnlyLocal": {
"message": "Само локалните стилове"
"message": "Само локалните стилове",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"manageOnlyLocalTooltip": {
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)"
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)",
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
},
"manageOnlyNonUsercss": {
"message": "Само не-Потребителскитеcss стилове"
"message": "Само не-Потребителскитеcss стилове",
"description": "Checkbox to show only non-Usercss (standard) styles"
},
"manageOnlyUpdates": {
"message": "Само с обновления или проблеми"
"message": "Само с обновления или проблеми",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
},
"manageOnlyUsercss": {
"message": "Само Потребителскиcss стилове"
"message": "Само Потребителскиcss стилове",
"description": "Checkbox to show only Usercss styles"
}
}
}

View File

@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,23 @@
{
"addStyleLabel": {
"message": "Skriv ny stil"
"message": "Skriv ny stil",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Tilføj stil"
"message": "Tilføj stil",
"description": "Title of the page for adding styles"
},
"alphaChannel": {
"message": "Gennemsigtighed"
"message": "Gennemsigtighed",
"description": "Label of color's opacity"
},
"appliesAdd": {
"message": "Tilføj"
"message": "Tilføj",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Anvendes på: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -20,81 +25,107 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "og mere"
"message": "og mere",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "URL'er på domænet"
"message": "URL'er på domænet",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på."
"message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "Anvendes på"
"message": "Anvendes på",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesLineWidgetLabel": {
"message": "Vis 'Anvendes på'-info"
"message": "Vis 'Anvendes på'-info",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"appliesRegexpOption": {
"message": "URL'er der matcher regexp'en"
"message": "URL'er der matcher regexp'en",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Fjern"
"message": "Fjern",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesRemoveError": {
"message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse"
"message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"appliesSpecify": {
"message": "Specificér"
"message": "Specificér",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Alt"
"message": "Alt",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlPrefixOption": {
"message": "URL'er der starter med"
"message": "URL'er der starter med",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"applyAllUpdates": {
"message": "Anvend alle opdateringer"
"message": "Anvend alle opdateringer",
"description": "Label for the button to apply all detected updates"
},
"author": {
"message": "Forfatter"
"message": "Forfatter",
"description": "Label for the style author"
},
"backupMessage": {
"message": "Vælg en fil eller træk og slip til denne side.",
"description": "Message for backup"
},
"bckpInstStyles": {
"message": "Eksportér stil"
"message": "Eksportér stil",
"description": ""
},
"checkAllUpdates": {
"message": "Tjek alle stiler for opdateringer"
"message": "Tjek alle stiler for opdateringer",
"description": "Label for the button to check all styles for updates"
},
"checkAllUpdatesForce": {
"message": "Tjek igen, jeg redigerede ikke nogen stil!"
"message": "Tjek igen, jeg redigerede ikke nogen stil!",
"description": "Label for the button to apply all detected updates"
},
"checkForUpdate": {
"message": "Tjek efter opdatering"
"message": "Tjek efter opdatering",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "Tjekker..."
"message": "Tjekker...",
"description": "Text to display when checking a style for an update"
},
"clickToUninstall": {
"message": "Klik for at afinstallere"
"message": "Klik for at afinstallere",
"description": "Label for the overlay on a style thumbnail when installed via inline search in the popup"
},
"cm_autoCloseBrackets": {
"message": "Luk automatisk paranteser og citationstegn"
"message": "Luk automatisk paranteser og citationstegn",
"description": "Label for the checkbox in the style editor."
},
"cm_autoCloseBracketsTooltip": {
"message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\""
"message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\"",
"description": "Label for the checkbox in the style editor."
},
"cm_autocompleteOnTyping": {
"message": "Autoudfyld på indtastning"
"message": "Autoudfyld på indtastning",
"description": "Label for the checkbox in the style editor."
},
"cm_colorpicker": {
"message": "Farvevælgere for CSS-farver"
"message": "Farvevælgere for CSS-farver",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"cm_indentWithTabs": {
"message": "Brug tabs med smart indrykning"
"message": "Brug tabs med smart indrykning",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
},
"cm_keyMap": {
"message": "Tastegenveje"
},
"genericAdd": {
"message": "Tilføj"
"message": "Tastegenveje",
"description": "Label for the drop-down list controlling the keymap for the style editor."
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,4 @@
{
"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": {
"message": "Write new style",
"description": "Label for the button to go to the add style page"
@ -93,8 +89,8 @@
"description": "Heading for backup"
},
"backupMessage": {
"message": "To import the backup file, drag'n'drop it into this page or click the Import button.\n\nTo export a compatible backup for Stylus older than 1.5.18, right-click or shift-click the Export button.",
"description": "Text for Backup section's (i) in the manager"
"message": "Select a file or drag and drop to this page.",
"description": "Message for backup"
},
"bckpInstStyles": {
"message": "Export styles"
@ -186,10 +182,6 @@
"message": "Theme",
"description": "Label for the style editor's CSS theme."
},
"cm_arrowKeysTraverse": {
"message": "Arrow keys ↑↓ traverse sections",
"description": "Label for the option in the editor."
},
"colorpickerPaletteHint": {
"message": "Right-click a swatch to cycle through its source lines"
},
@ -253,12 +245,6 @@
"message": "Yes",
"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": {
"message": "Copied to clipboard",
"description": "Message shown when content has been copied to the clipboard"
@ -276,39 +262,39 @@
},
"dateAbbrDay": {
"message": "$value$d",
"description": "Day suffix in a short relative date, for example: 8d",
"placeholders": {
"value": {
"content": "$1"
}
},
"description": "Day suffix in a short relative date, for example: 8d"
}
},
"dateAbbrHour": {
"message": "$value$h",
"description": "Hour suffix in a short relative date, for example: 8h",
"placeholders": {
"value": {
"content": "$1"
}
},
"description": "Hour suffix in a short relative date, for example: 8h"
}
},
"dateAbbrMonth": {
"message": "$value$m",
"description": "Month suffix in a short relative date, for example: 8m",
"placeholders": {
"value": {
"content": "$1"
}
},
"description": "Month suffix in a short relative date, for example: 8m"
}
},
"dateAbbrYear": {
"message": "$value$y",
"description": "Year suffix in a short relative date, for example: 8y",
"placeholders": {
"value": {
"content": "$1"
}
},
"description": "Year suffix in a short relative date, for example: 8y"
}
},
"dateInstalled": {
"message": "Date installed",
@ -340,29 +326,12 @@
},
"disableAllStyles": {
"message": "Turn all styles off",
"description": "Label for the checkbox that turns all styles off."
},
"disableAllStylesOff": {
"message": "Styles are turned off",
"description": "Label for the checkbox that turns all styles off when it's checked."
"description": "Label for the checkbox that turns all enabled styles off."
},
"disableStyleLabel": {
"message": "Disable",
"description": "Label for the button to disable a style"
},
"draftTitle": {
"message": "Draft recovery, created $date$",
"placeholders": {
"date": {
"content": "$1"
}
},
"description": "Title of the modal displayed in the editor when an unsaved draft is found, the $date$ looks like '1 hour ago' in user's current UI language"
},
"draftAction": {
"message": "Choose 'Yes' to load this draft or 'No' to discard it.",
"description": "Displayed in the editor after the browser/extension crashed"
},
"dragDropMessage": {
"message": "Drop your backup file anywhere on this page to import.",
"description": "Drag'n'drop message"
@ -396,9 +365,6 @@
},
"description": "Title of the page for editing styles"
},
"editorSettings": {
"message": "Editor settings"
},
"enableStyleLabel": {
"message": "Enable",
"description": "Label for the button to enable a style"
@ -413,12 +379,6 @@
"message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"exportCompatible": {
"message": "Export (compatible mode)"
},
"exportSavedSuccess": {
"message": "File saved with success"
},
"externalFeedback": {
"message": "Feedback",
"description": "Label for the external link to send feedback for the style"
@ -459,6 +419,18 @@
"message": "Find styles",
"description": "Text for a link that gets a list of styles for the current site"
},
"findStylesForSite": {
"message": "Find more styles for this site",
"description": "Text for a link that gets a list of styles for the current site"
},
"findStylesInline": {
"message": "Inline",
"description": "Text for a checkbox that opens search results 'inline' (within the Stylus popup window)"
},
"findStylesInlineTooltip": {
"message": "Display search results inside this window.",
"description": "Text for a checkbox that displays search results within the Stylus popup."
},
"genericAdd": {
"message": "Add",
"description": "Used in various places for an action that adds something"
@ -467,9 +439,6 @@
"message": "Clone",
"description": "Used in various places for an action that clones something"
},
"genericDescription": {
"message": "Description"
},
"genericDisabledLabel": {
"message": "Disabled",
"description": "Used in various lists/options to indicate that something is disabled"
@ -502,13 +471,6 @@
"message": "Saved",
"description": "Used in various parts of the UI to indicate that something was saved"
},
"genericSize": {
"message": "Size"
},
"genericTest": {
"message": "Test",
"description": "Label for the action that runs some test e.g. opens the regexp tester panel in the editor"
},
"genericTitle": {
"message": "Title",
"description": "Used in various parts of the UI to indicate the title of something"
@ -517,13 +479,6 @@
"message": "Unknown",
"description": "Used in various parts of the UI to indicate if something is unknown (e.g. an unknown date)"
},
"gettingStyles": {
"message": "Getting all styles..."
},
"headerResizerHint": {
"message": "Hold Shift to resize only in this type of UI, i.e. editor, manager, installer",
"description": "Tooltip for the header panel resizer"
},
"helpAlt": {
"message": "Help",
"description": "Alternate text for help buttons"
@ -611,7 +566,7 @@
"description": "Label for install button"
},
"installButtonInstalled": {
"message": "Style is installed",
"message": "Style installed",
"description": "Text displayed when the style is successfully installed"
},
"installButtonReinstall": {
@ -651,18 +606,6 @@
"message": "Get styles",
"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": {
"message": "Wiki",
"description": "Wiki link text on the manage page e.g. https://github.com/openstyles/stylus/wiki"
@ -751,7 +694,7 @@
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"manageFaviconsHelp": {
"message": "Stylus uses an external service https://icons.duckduckgo.com",
"message": "Stylus uses an external service https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"manageFilters": {
@ -766,9 +709,6 @@
"message": "Number of applies-to items",
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"manageMinColumnWidth": {
"message": "Minimum column width (in pixels; 9999 disables multi-column mode)"
},
"manageNewStyleAsUsercss": {
"message": "as Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
@ -823,105 +763,105 @@
},
"meta_invalidColor": {
"message": "Invalid @var color: $color$ is not a color",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"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": {
"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": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeDefault": {
"message": "Invalid @var $type$: default value is null",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeMax": {
"message": "Invalid @var $type$: default value is larger than the maximum",
"placeholders": {
"type": {
"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",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeStep": {
"message": "Invalid @var $type$: default value is not a mutiple of the step",
"placeholders": {
"type": {
"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",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeUnits": {
"message": "Invalid @var $type$: '$units$' is not a valid unit",
"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",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
}
},
"meta_invalidRangeDefault": {
"message": "Invalid @var $type$: default value is null",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"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": {
"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": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeStep": {
"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": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeUnits": {
"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": {
"type": {
"content": "$1"
},
"units": {
"content": "$2"
}
}
},
"meta_invalidSelect": {
"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"
},
"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": {
"message": "Invalid @var select: options list is empty",
"description": "Error displayed when the value of @var select is invalid"
@ -938,30 +878,35 @@
"message": "Invalid @var select: option name is duplicated",
"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": {
"message": "Invalid @var select: value doesn't exist in the option list",
"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": {
"message": "Invalid URL protocol. Only http and https are allowed: $protocol$",
"description": "Error displayed when the protocol of the URL is invalid",
"placeholders": {
"protocol": {
"content": "$1"
}
},
"description": "Error displayed when the protocol of the URL is invalid"
}
},
"meta_invalidVersion": {
"message": "Invalid version number",
"description": "Error displayed when @version is invalid"
"message": "Invalid version number. The value doesn't match SemVer pattern: $version$",
"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": {
"message": "Expect a word",
@ -969,12 +914,12 @@
},
"meta_missingChar": {
"message": "Expect characters: $chars$",
"description": "Error displayed when the value is expected to be some characters",
"placeholders": {
"chars": {
"content": "$1"
}
},
"description": "Error displayed when the value is expected to be some characters"
}
},
"meta_missingEOT": {
"message": "Expect EOT data",
@ -982,66 +927,51 @@
},
"meta_missingMandatory": {
"message": "Missing mandatory metadata: $keys$",
"description": "Error displayed when mandatory keys are missing",
"placeholders": {
"keys": {
"content": "$1"
}
},
"description": "Error displayed when mandatory keys are missing"
}
},
"meta_unknownJSONLiteral": {
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
"description": "Error displayed when JSON value is invalid",
"placeholders": {
"literal": {
"content": "$1"
}
},
"description": "Error displayed when JSON value is invalid"
}
},
"meta_unknownMeta": {
"message": "Unknown metadata: $key$",
"description": "Error displayed when unknown metadata is parsed",
"placeholders": {
"key": {
"content": "$1"
}
},
"description": "Error displayed when unknown metadata is parsed"
}
},
"meta_unknownMetaTypo": {
"message": "Maybe @$keyOk$? Unknown metadata: @$keyErr$",
"meta_unknownVarType": {
"message": "Unknown @$varkey$ type: $vartype$",
"description": "Error displayed when unknown variable type is parsed",
"placeholders": {
"keyErr": {
"varkey": {
"content": "$1"
},
"keyOk": {
"vartype": {
"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": {
"message": "Unknown @preprocessor: $preprocessor$",
"description": "Error displayed when unknown @preprocessor is parsed",
"placeholders": {
"preprocessor": {
"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": {
"message": "No styles installed for this site.",
@ -1079,24 +1009,9 @@
"message": "Exposes the top site domain in each iframe.\nEnables writing iframe-specific CSS like this:\nhtml[stylus-iframe$$=\"twitter.com\"] h1 { display:none }",
"description": "Add attribute to iframe; make sure to include the double $$ in the css example, or the `$=` will be omitted in the displayed text."
},
"optionsAdvancedExposeStyleName": {
"message": "Expose style name"
},
"optionsAdvancedExposeStyleNameNote": {
"message": "Exposes the style name in the page to facilitate debugging of styles in devtools. Please reload the tab(s) to apply the new setting."
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Write new style as usercss"
},
"optionsAdvancedAutoSwitchSchemeNever": {
"message": "Disabled. The dark/light setting in styles is ignored."
},
"optionsAdvancedAutoSwitchSchemeBySystem": {
"message": "By system preference"
},
"optionsAdvancedAutoSwitchSchemeByTime": {
"message": "By night time:"
},
"optionsAdvancedPatchCsp": {
"message": "Patch <code>CSP</code> to allow style assets"
},
@ -1130,19 +1045,16 @@
"optionsCustomizePopup": {
"message": "Popup"
},
"optionsCustomizeSync": {
"message": "Sync to cloud"
},
"optionsCustomizeUpdate": {
"message": "Updates"
},
"optionsCustomizeSync": {
"message": "Sync to cloud"
},
"optionsHeading": {
"message": "Options",
"description": "Heading for options section on manage page."
},
"optionsIconAuto": {
"message": "Match the Dark/Light mode"
},
"optionsIconDark": {
"message": "Dark browser themes"
},
@ -1165,36 +1077,33 @@
"message": "Reset options"
},
"optionsStylusThemes": {
"message": "Click Stylus icon in the browser toolbar on any Stylus page including this one, then click 'Find styles'"
"message": "Find a Stylus UI theme"
},
"optionsSubheading": {
"message": "More Options",
"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": {
"message": "Connect"
},
"optionsSyncDisconnect": {
"message": "Disconnect"
},
"optionsSyncSyncNow": {
"message": "Sync now"
},
"optionsSyncLogin": {
"message": "Login"
},
"optionsSyncNone": {
"message": "None"
},
"optionsSyncStatusConnected": {
"message": "Connected"
},
"optionsSyncStatusConnecting": {
"message": "Connecting..."
},
"optionsSyncStatusDisconnected": {
"message": "Disconnected"
},
"optionsSyncStatusDisconnecting": {
"message": "Disconnecting..."
},
"optionsSyncStatusPull": {
"message": "Pulling style $loaded$ of $total$",
"placeholders": {
@ -1217,32 +1126,20 @@
}
}
},
"optionsSyncUsername": {
"message": "Username"
},
"optionsSyncPassword": {
"message": "Password"
},
"optionsSyncUrl": {
"message": "URL"
},
"optionsSyncStatusRelogin": {
"message": "Session expired, please login again."
},
"optionsSyncStatusSyncing": {
"message": "Syncing..."
},
"optionsSyncSyncNow": {
"message": "Sync now"
"optionsSyncStatusConnecting": {
"message": "Connecting..."
},
"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."
"optionsSyncStatusConnected": {
"message": "Connected"
},
"optionsUpdateInterval": {
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
"optionsSyncStatusDisconnecting": {
"message": "Disconnecting..."
},
"overwriteFileExport": {
"message": "Do you want to overwrite an existing file?"
"optionsSyncStatusDisconnected": {
"message": "Disconnected"
},
"paginationCurrent": {
"message": "Current page",
@ -1285,10 +1182,6 @@
"message": "Click to see available hotkeys",
"description": "Tooltip displayed when hovering the right edge of the extension popup"
},
"popupManageSiteStyles": {
"message": "Manage site styles",
"description": "Item in the dropdown menu for the 'Manage' button in the popup that opens manager with styles applicable for current site."
},
"popupManageTooltip": {
"message": "Shift-click or right-click opens manager with styles applicable for current site",
"description": "Tooltip for the 'Manage' button in the popup."
@ -1313,21 +1206,6 @@
"message": "Styles before commands",
"description": "Label for the checkbox controlling section order in the popup."
},
"preferScheme": {
"message": "Dark/Light mode preference"
},
"preferSchemeAlways": {
"message": "Currently ignored (the style always applies) because the global Dark/Light mode is disabled"
},
"preferSchemeDark": {
"message": "Dark"
},
"preferSchemeLight": {
"message": "Light"
},
"preferSchemeNone": {
"message": "None (always applied)"
},
"prefShowBadge": {
"message": "Number of styles active for the current site",
"description": "Label for the checkbox controlling toolbar badge text."
@ -1340,34 +1218,9 @@
"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."
},
"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": {
"message": "Reload",
"description": "Context menu to reload the extension when installed in developer mode"
"message": "Reload Stylus extension",
"description": "Context menu reload"
},
"replace": {
"message": "Replace",
@ -1381,18 +1234,9 @@
"message": "Replace with",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"restoreTemplate": {
"message": "Restore the default template.\n\n(The currently open editor pages won't change.)"
},
"retrieveBckp": {
"message": "Import styles"
},
"retrieveDropboxSync": {
"message": "Dropbox Import"
},
"saveAsTemplate": {
"message": "Save as template"
},
"search": {
"message": "Search",
"description": "Label before the search input field in the editor shown on Ctrl-F"
@ -1413,6 +1257,10 @@
"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"
},
"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": {
"message": "Use /re/ syntax for regexp search",
"description": "Label after the search input field in the editor shown on Ctrl-F"
@ -1443,10 +1291,6 @@
"message": "Weekly installs",
"description": "Text for label that shows the number of times a search result was installed during last week"
},
"searchStyleQueryHint": {
"message": "Search style names (case-sensitively if an uppercase letter is used):\nsome words - all these words in any order\n\"some phrase\" - this exact phrase without quotes\n/foo.*bar/i - regular expression without spaces (use \\s instead)",
"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."
@ -1491,10 +1335,6 @@
"message": "Sections",
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
},
"settings": {
"message": "Settings",
"description": "Generic label/title for settings"
},
"shortcuts": {
"message": "Shortcuts",
"description": "Go to shortcut configuration"
@ -1502,9 +1342,6 @@
"shortcutsNote": {
"message": "Define keyboard shortcuts"
},
"shortcutsNoteFF": {
"message": "In Firefox 66+ you can open the built-in shortcuts UI manually:\n1) right-click Stylus icon in the toolbar and choose 'Manage'\n(alternatively, open about:addons via the main menu or Ctrl-Shift-A),\n2) in the page that opens click the cog wheel icon in the top right corner,\n3) choose 'Manage extension shortcuts'.\n\nYou can also customize the shortcuts here."
},
"sortDateNewestFirst": {
"message": "newest first",
"description": "Text added to indicate that sorting a date would add the newest entries at the top"
@ -1614,19 +1451,10 @@
"message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
},
"styleName": {
"message": "Style name"
},
"styleNotAppliedRegexpProblemTooltip": {
"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"
},
"styleNotAppliedSchemeDark": {
"message": "This style is only applied in Dark Mode"
},
"styleNotAppliedSchemeLight": {
"message": "This style is only applied in Light Mode"
},
"styleRegexpInvalidExplanation": {
"message": "Some 'regexp()' rules that could not be compiled at all."
},
@ -1637,6 +1465,10 @@
"message": "Number of sections not applied due to incorrect usage of 'regexp()'",
"description": "Tooltip in the popup for styles that were applied only partially"
},
"styleRegexpTestButton": {
"message": "RegExp test",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
},
"styleRegexpTestFull": {
"message": "Matching tabs",
"description": "RegExp test report: label for the fully matching expressions"
@ -1665,10 +1497,6 @@
"message": "Save",
"description": "Label for save button for style editing"
},
"styleSettings": {
"message": "Style settings",
"description": "Label/title for style settings dialog"
},
"styleToMozillaFormatHelp": {
"message": "The Mozilla format of the code can be submitted to userstyles.org and used with the classic Stylish for Firefox",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
@ -1698,55 +1526,6 @@
"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"
},
"styleUpdateUrlLabel": {
"message": "Update URL"
},
"stylePreferSchemeLabel": {
"message": "Dark/Light mode"
},
"styleIncludeLabel": {
"message": "Custom included sites"
},
"styleInjectionImportance": {
"message": "Toggle style's importance"
},
"styleInjectionOrder": {
"message": "Style injection order",
"description": "Tooltip for the button in the manager to open the dialog and also the title of this dialog"
},
"styleInjectionOrderHint": {
"message": "Drag'n'drop a style to change its position. Styles are injected sequentially in the order shown below so a style further down the list can override the earlier styles.",
"description": "Hint in the injection order dialog in the manager"
},
"styleInjectionOrderHint_prio": {
"message": "Important styles listed below are always injected last so they can override any newly installed styles. Click the style's mark to toggle its importance.",
"description": "Hint at the bottom of the injection order dialog in the manager"
},
"styleExcludeLabel": {
"message": "Custom excluded sites"
},
"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. You have been logged out.\nTry to re-login in Stylus options.",
"description": "Tooltip for the toolbar icon"
},
"syncErrorLock": {
"message": "The database is already in use. The lock will expire at $TIME$",
"placeholders": {
"time": {
"content": "$1"
}
}
},
"syncStorageErrorSaving": {
"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"
@ -1771,6 +1550,14 @@
"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"
},
"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": {
"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"
@ -1779,16 +1566,9 @@
"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"
},
"unreachableMozSiteHint": {
"message": "In Firefox 60 and newer you need to remove this domain from <extensions.webextensions.restrictedDomains> in <about:config>.",
"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..."
"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"
},
"updateAllCheckSucceededNoUpdate": {
"message": "No updates found.",
@ -1842,9 +1622,6 @@
"message": "Updates installed:",
"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": {
"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."
@ -1859,6 +1636,10 @@
"usercssReplaceTemplateConfirmation": {
"message": "Replace the default template for new Usercss styles with the current code?"
},
"usercssReplaceTemplateName": {
"message": "Empty @name replaces the default template",
"description": "The text shown after @name when creating a new Usercss style"
},
"usercssReplaceTemplateSectionBody": {
"message": "Insert code here...",
"description": "The code placeholder comment in a new style created by clicking 'Write style' in the popup"
@ -1875,7 +1656,43 @@
"message": "this 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": {
"message": "Zipping styles..."
},
"unzipStyles": {
"message": "Unzipping styles..."
},
"readingStyles": {
"message": "Reading styles..."
},
"uploadingFile": {
"message": "Uploading File..."
}
}

View File

@ -1,51 +1,67 @@
{
"appliesRemoveError": {
"message": "Cannot remove last 'applies to' entry"
"message": "Cannot remove last 'applies to' entry",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"checkAllUpdatesForce": {
"message": "Check again—I didn't edit any styles!"
"message": "Check again—I didn't edit any styles!",
"description": "Label for the button to apply all detected updates"
},
"cm_autoCloseBrackets": {
"message": "Auto-close brackets and quotes"
"message": "Auto-close brackets and quotes",
"description": "Label for the checkbox in the style editor."
},
"cm_colorpicker": {
"message": "Colour pickers for CSS colours"
"message": "Colour pickers for CSS colours",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"cm_resizeGripHint": {
"message": "Double-click to maximise/restore the height"
"message": "Double-click to maximise/restore the height",
"description": "Tooltip for the resize grip in style editor"
},
"colorpickerTooltip": {
"message": "Open colour picker"
"message": "Open colour picker",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"description": {
"message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites."
"message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites.",
"description": "Extension description"
},
"editGotoLine": {
"message": "Go to line (or line:col)"
"message": "Go to line (or line:col)",
"description": "Go to line or line:column on Ctrl-G in style code editor"
},
"editStyleHeading": {
"message": "Edit style"
"message": "Edit style",
"description": "Title of the page for editing styles"
},
"license": {
"message": "Licence"
"message": "Licence",
"description": "Label for the license"
},
"manageFaviconsGray": {
"message": "Greyed out"
"message": "Greyed out",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"optionsBadgeDisabled": {
"message": "Background colour when disabled"
"message": "Background colour when disabled",
"description": ""
},
"optionsBadgeNormal": {
"message": "Background colour"
"message": "Background colour",
"description": ""
},
"optionsUpdateImportNote": {
"message": "When importing style backups from an old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
"message": "When importing style backups from an old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated.",
"description": ""
},
"optionsUpdateInterval": {
"message": "Userstyle auto-update interval in hours (specify 0 to disable)"
"message": "Userstyle auto-update interval in hours (specify 0 to disable)",
"description": ""
},
"styleInstallFailed": {
"message": "Failed to install userstyle\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
@ -53,12 +69,15 @@
}
},
"styleRegexpPartialExplanation": {
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome, which incorrectly checks 'regexp()' rules since the very first version (known bug)."
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome, which incorrectly checks 'regexp()' rules since the very first version (known bug).",
"description": ""
},
"styleUpdateDiscardChanges": {
"message": "The style has been changed outside the editor. Would you like to reload the style?"
"message": "The style has been changed outside the editor. Would you like to reload the style?",
"description": "Confirmation to update the style in the editor"
},
"usercssConfigIncomplete": {
"message": "The style was updated or deleted after the configuration dialogue was shown. These variables were not saved to avoid corrupting the style's metadata:"
"message": "The style was updated or deleted after the configuration dialogue was shown. These variables were not saved to avoid corrupting the style's metadata:",
"description": ""
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,19 @@
{
"addStyleLabel": {
"message": "Uusi Tyyli"
"message": "Uusi Tyyli",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Lisää Tyyli"
"message": "Lisää Tyyli",
"description": "Title of the page for adding styles"
},
"appliesAdd": {
"message": "Lisää"
"message": "Lisää",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Kooskee: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -17,70 +21,80 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "ja lisää"
"message": "ja lisää",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "URL ositteita domainilla"
"message": "URL ositteita domainilla",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee."
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "Koskee"
"message": "Koskee",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesRegexpOption": {
"message": "URL ositteet jotka vastaavat regexpiä"
"message": "URL ositteet jotka vastaavat regexpiä",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Poista"
"message": "Poista",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "Tarkenna"
"message": "Tarkenna",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Kaikki"
"message": "Kaikki",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlPrefixOption": {
"message": "URL osoitteet jotka alkavat"
"message": "URL osoitteet jotka alkavat",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"checkAllUpdates": {
"message": "Tarkista kaikki tyylit päivityksien varalta"
"message": "Tarkista kaikki tyylit päivityksien varalta",
"description": "Label for the button to check all styles for updates"
},
"checkForUpdate": {
"message": "Hae päivityksiä"
"message": "Hae päivityksiä",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "Tarkistetaan..."
},
"confirmDelete": {
"message": "Poista"
},
"confirmSave": {
"message": "Tallenna"
"message": "Tarkistetaan...",
"description": "Text to display when checking a style for an update"
},
"deleteStyleConfirm": {
"message": "Oletko varma että haluat poistaa tämän tyylin?"
"message": "Oletko varma että haluat poistaa tämän tyylin?",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "Poista"
"message": "Poista",
"description": "Label for the button to delete a style"
},
"description": {
"message": "Uudelleen stailaa netti Stylusillä, käyttäjän tyyli hallintapaneelilla. Stylus antaa sinun helposti asentaa teemoja ja skinejä palvelluille kuten Google, Facebook, YouTube, Orkut, ja monelle, monelle muulle sivustolle."
"message": "Uudelleen stailaa netti Stylusillä, käyttäjän tyyli hallintapaneelilla. Stylus antaa sinun helposti asentaa teemoja ja skinejä palvelluille kuten Google, Facebook, YouTube, Orkut, ja monelle, monelle muulle sivustolle.",
"description": "Extension description"
},
"disableStyleLabel": {
"message": "Poista Käytöstä"
},
"editDeleteText": {
"message": "Poista"
"message": "Poista Käytöstä",
"description": "Label for the button to disable a style"
},
"editStyleHeading": {
"message": "Muokkaa Tyyliä"
"message": "Muokkaa Tyyliä",
"description": "Title of the page for editing styles"
},
"editStyleLabel": {
"message": "Muokkaa"
"message": "Muokkaa",
"description": "Label for the button to go to the edit style page"
},
"editStyleTitle": {
"message": "Muokkaa Tyyliä $stylename$",
"description": "Title of the page for editing styles",
"placeholders": {
"stylename": {
"content": "$1"
@ -88,64 +102,76 @@
}
},
"enableStyleLabel": {
"message": "Aktivoi"
"message": "Aktivoi",
"description": "Label for the button to enable a style"
},
"genericAdd": {
"message": "Lisää"
},
"genericEnabledLabel": {
"message": "Aktivoitu"
"findStylesForSite": {
"message": "Hae lisää tyylejä tälle sivustolle",
"description": "Text for a link that gets a list of styles for the current site"
},
"helpAlt": {
"message": "Apu"
"message": "Apu",
"description": "Alternate text for help buttons"
},
"installUpdate": {
"message": "Asenna päivitys"
"message": "Asenna päivitys",
"description": "Label for the button to install an update for a single style"
},
"manageHeading": {
"message": "Asennetut Tyylit"
"message": "Asennetut Tyylit",
"description": "Heading for the manage page"
},
"manageTitle": {
"message": "Tyylikäs"
"message": "Tyylikäs",
"description": "Title for the manage page"
},
"noStylesForSite": {
"message": "Ei asennettuja tyylejä tällä sivustolla."
"message": "Ei asennettuja tyylejä tällä sivustolla.",
"description": "Text displayed when no styles are installed for the current site"
},
"openManage": {
"message": "Hallitse asennettuja tyylejä"
"message": "Hallitse asennettuja tyylejä",
"description": "Link to open the manage page."
},
"popupStylesFirst": {
"message": "List styles before commands in the toolbar button menu"
"message": "List styles before commands in the toolbar button menu",
"description": "Label for the checkbox controlling section order in the popup."
},
"prefShowBadge": {
"message": "Show number of styles active for the current site on the toolbar button"
"message": "Show number of styles active for the current site on the toolbar button",
"description": "Label for the checkbox controlling toolbar badge text."
},
"sectionAdd": {
"message": "Lisää uusi osio"
"message": "Lisää uusi osio",
"description": "Label for the button to add a section"
},
"sectionCode": {
"message": "Koodi"
"message": "Koodi",
"description": "Label for the code for a section"
},
"sectionRemove": {
"message": "Poista osio"
},
"sections": {
"message": "Osiot"
"message": "Poista osio",
"description": "Label for the button to remove a section"
},
"styleBadRegexp": {
"message": "Regexp ei kelpaa."
"message": "Regexp ei kelpaa.",
"description": "Validation message for a bad regexp in a style"
},
"styleCancelEditLabel": {
"message": "Takaisin hallintapaneeliin"
"message": "Takaisin hallintapaneeliin",
"description": "Label for cancel button for style editing"
},
"styleChangesNotSaved": {
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta."
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta.",
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
},
"styleEnabledLabel": {
"message": "Aktivoitu"
"message": "Aktivoitu",
"description": "Label for the enabled state of styles"
},
"styleInstall": {
"message": "Asennetaanko '$stylename$' Stylusiin?",
"description": "Confirmation when installing a style",
"placeholders": {
"stylename": {
"content": "$1"
@ -153,19 +179,24 @@
}
},
"styleMissingName": {
"message": "Syötä nimi"
"message": "Syötä nimi",
"description": "Error displayed when user saves without providing a name"
},
"styleSaveLabel": {
"message": "Tallenna"
"message": "Tallenna",
"description": "Label for save button for style editing"
},
"styleToMozillaFormatHelp": {
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin."
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin.",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
},
"updateAllCheckSucceededNoUpdate": {
"message": "All styles are up to date."
"message": "All styles are up to date.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"updateCheckFailBadResponseCode": {
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": {
"code": {
"content": "$1"
@ -173,12 +204,15 @@
}
},
"updateCheckFailServerUnreachable": {
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen."
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen.",
"description": "Text that displays when an update check failed because the update server is unreachable"
},
"updateCheckSucceededNoUpdate": {
"message": "Tyyli on ajan tasalla."
"message": "Tyyli on ajan tasalla.",
"description": "Text that displays when an update check completed and no update is available"
},
"updateCompleted": {
"message": "Päivitys suoritettu."
"message": "Päivitys suoritettu.",
"description": "Text that displays when an update completed"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,19 @@
{
"addStyleLabel": {
"message": "Nije styl skriuwe"
"message": "Nije styl skriuwe",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Styl tafoegje"
"message": "Styl tafoegje",
"description": "Title of the page for adding styles"
},
"appliesAdd": {
"message": "Tafoegje"
"message": "Tafoegje",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Fan tapassing op: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -17,81 +21,107 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "en mear"
"message": "en mear",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "URLs op it domein"
"message": "URLs op it domein",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "Brûk de Fan tapassing op-funksjes om de URLs foar de koade yn dizze seksje te beheinen."
"message": "Brûk de Fan tapassing op-funksjes om de URLs foar de koade yn dizze seksje te beheinen.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "Fan tapassing op"
"message": "Fan tapassing op",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesRegexpOption": {
"message": "URLs oerienkommend mei de regexp"
"message": "URLs oerienkommend mei de regexp",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Fuortsmite"
"message": "Fuortsmite",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "Spesifisearje"
"message": "Spesifisearje",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Alles"
"message": "Alles",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlPrefixOption": {
"message": "URLs begjinnend mei"
"message": "URLs begjinnend mei",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"applyAllUpdates": {
"message": "Alle fernijingen tapasse"
"message": "Alle fernijingen tapasse",
"description": "Label for the button to apply all detected updates"
},
"checkAllUpdates": {
"message": "Alle stilen kontrolearje op fernijingen"
"message": "Alle stilen kontrolearje op fernijingen",
"description": "Label for the button to check all styles for updates"
},
"checkForUpdate": {
"message": "Kontrolearje op fernijing"
"message": "Kontrolearje op fernijing",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "Kontrolearje..."
"message": "Kontrolearje...",
"description": "Text to display when checking a style for an update"
},
"cm_indentWithTabs": {
"message": "Ljepblêden mei tûke ynspringing brûke"
"message": "Ljepblêden mei tûke ynspringing brûke",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
},
"cm_keyMap": {
"message": "Toetseboerdyndieling"
"message": "Toetseboerdyndieling",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"cm_lineWrapping": {
"message": "Teksttebekrin"
"message": "Teksttebekrin",
"description": "Label for the checkbox controlling word wrap option for the style editor."
},
"cm_smartIndent": {
"message": "Tûke ynspringing brûke"
"message": "Tûke ynspringing brûke",
"description": "Label for the checkbox controlling smart indentation option for the style editor."
},
"cm_tabSize": {
"message": "Ljepblêdgrutte"
"message": "Ljepblêdgrutte",
"description": "Label for the text box controlling tab size option for the style editor."
},
"cm_theme": {
"message": "Tema"
"message": "Tema",
"description": "Label for the style editor's CSS theme."
},
"confirmNo": {
"message": "Nee"
"message": "Nee",
"description": "'No' button in a confirm dialog"
},
"confirmStop": {
"message": "Stoppe"
"message": "Stoppe",
"description": "'Stop' button in a confirm dialog"
},
"confirmYes": {
"message": "Ja"
"message": "Ja",
"description": "'Yes' button in a confirm dialog"
},
"dbError": {
"message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?"
"message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?",
"description": "Prompt when a DB error is encountered"
},
"defaultTheme": {
"message": "standert"
"message": "standert",
"description": "Default CodeMirror CSS theme option on the edit style page"
},
"deleteStyleConfirm": {
"message": "Binne jo wis dat jo dizze styl fuortsmite wolle?"
"message": "Binne jo wis dat jo dizze styl fuortsmite wolle?",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "Fuortsmite"
"message": "Fuortsmite",
"description": "Label for the button to delete a style"
}
}
}

View File

@ -1,15 +1,19 @@
{
"addStyleTitle": {
"message": "Engadir Estilo"
"message": "Engadir Estilo",
"description": "Title of the page for adding styles"
},
"alphaChannel": {
"message": "Opacidade"
"message": "Opacidade",
"description": "Label of color's opacity"
},
"appliesAdd": {
"message": "Engadir"
"message": "Engadir",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Aplica a: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -17,36 +21,47 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "e mais"
"message": "e mais",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "URLs no dominio"
"message": "URLs no dominio",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesLabel": {
"message": "Aplica para"
"message": "Aplica para",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesLineWidgetWarning": {
"message": "Non funciona con CSS minificado"
"message": "Non funciona con CSS minificado",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"appliesRegexpOption": {
"message": "URLs que concorden co regexp"
"message": "URLs que concorden co regexp",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Suprimir"
"message": "Suprimir",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "Especificar"
"message": "Especificar",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Todo"
"message": "Todo",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlPrefixOption": {
"message": "URLs que comecen por"
"message": "URLs que comecen por",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"applyAllUpdates": {
"message": "Aplicar tódalas actualizacións"
"message": "Aplicar tódalas actualizacións",
"description": "Label for the button to apply all detected updates"
},
"author": {
"message": "Autor"
"message": "Autor",
"description": "Label for the style author"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,19 @@
{
"addStyleLabel": {
"message": "Упиши нови стил"
"message": "Упиши нови стил",
"description": "Label for the button to go to the add style page"
},
"addStyleTitle": {
"message": "Додај стил"
"message": "Додај стил",
"description": "Title of the page for adding styles"
},
"appliesAdd": {
"message": "Додај"
"message": "Додај",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "Примењује се на: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -17,115 +21,140 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "и још"
"message": "и још",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesDomainOption": {
"message": "УРЛ адресе на домену"
"message": "УРЛ адресе на домену",
"description": "Option to make the style apply to the entered string as a domain"
},
"appliesHelp": {
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује."
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.",
"description": "Help text for 'applies to' section"
},
"appliesLabel": {
"message": "Примењује се на"
"message": "Примењује се на",
"description": "Label for 'applies to' fields on the edit/add screen"
},
"appliesRegexpOption": {
"message": "УРЛ адресе које одговарају регуларном изразу"
"message": "УРЛ адресе које одговарају регуларном изразу",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"appliesRemove": {
"message": "Уклони"
"message": "Уклони",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesSpecify": {
"message": "Детаљније"
"message": "Детаљније",
"description": "Label for the button to make a style apply only to specific sites"
},
"appliesToEverything": {
"message": "Све"
"message": "Све",
"description": "Text displayed for styles that apply to all sites"
},
"appliesUrlOption": {
"message": "УРЛ"
"message": "УРЛ",
"description": "Option to make the style apply to the entered string as a URL"
},
"appliesUrlPrefixOption": {
"message": "УРЛ адресе које почињу са"
"message": "УРЛ адресе које почињу са",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"applyAllUpdates": {
"message": "Примени сва ажурирања"
"message": "Примени сва ажурирања",
"description": "Label for the button to apply all detected updates"
},
"checkAllUpdates": {
"message": "Проверите ажурирања за све стилове"
"message": "Проверите ажурирања за све стилове",
"description": "Label for the button to check all styles for updates"
},
"checkForUpdate": {
"message": "Проверите ажурирање"
"message": "Проверите ажурирање",
"description": "Label for the button to check a single style for an update"
},
"checkingForUpdate": {
"message": "Проверавање..."
"message": "Проверавање...",
"description": "Text to display when checking a style for an update"
},
"cm_indentWithTabs": {
"message": "Користи картице са паметним увлачењем редова"
"message": "Користи картице са паметним увлачењем редова",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
},
"cm_keyMap": {
"message": "Мапа тастера"
"message": "Мапа тастера",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"cm_lineWrapping": {
"message": "Преламање текста"
"message": "Преламање текста",
"description": "Label for the checkbox controlling word wrap option for the style editor."
},
"cm_smartIndent": {
"message": "Користи паметно увлачење редова"
"message": "Користи паметно увлачење редова",
"description": "Label for the checkbox controlling smart indentation option for the style editor."
},
"cm_tabSize": {
"message": "Величина картице"
"message": "Величина картице",
"description": "Label for the text box controlling tab size option for the style editor."
},
"cm_theme": {
"message": "Тема"
},
"confirmDelete": {
"message": "Избриши"
"message": "Тема",
"description": "Label for the style editor's CSS theme."
},
"confirmNo": {
"message": "Не"
},
"confirmSave": {
"message": "Сачувај"
"message": "Не",
"description": "'No' button in a confirm dialog"
},
"confirmStop": {
"message": "Заустави"
"message": "Заустави",
"description": "'Stop' button in a confirm dialog"
},
"confirmYes": {
"message": "Да"
"message": "Да",
"description": "'Yes' button in a confirm dialog"
},
"dbError": {
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?"
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?",
"description": "Prompt when a DB error is encountered"
},
"defaultTheme": {
"message": "подразумевано"
"message": "подразумевано",
"description": "Default CodeMirror CSS theme option on the edit style page"
},
"deleteStyleConfirm": {
"message": "Да ли сте сигурни да желите да избришете овај стил?"
"message": "Да ли сте сигурни да желите да избришете овај стил?",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "Избриши"
"message": "Избриши",
"description": "Label for the button to delete a style"
},
"description": {
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове."
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.",
"description": "Extension description"
},
"disableAllStyles": {
"message": "Искључи све стилове"
"message": "Искључи све стилове",
"description": "Label for the checkbox that turns all enabled styles off."
},
"disableStyleLabel": {
"message": "Онемогући"
},
"editDeleteText": {
"message": "Избриши"
"message": "Онемогући",
"description": "Label for the button to disable a style"
},
"editGotoLine": {
"message": "Иди на ред (или line:col)"
"message": "Иди на ред (или line:col)",
"description": "Go to line or line:column on Ctrl-G in style code editor"
},
"editStyleHeading": {
"message": "Уреди стил"
"message": "Уреди стил",
"description": "Title of the page for editing styles"
},
"editStyleLabel": {
"message": "Уреди"
"message": "Уреди",
"description": "Label for the button to go to the edit style page"
},
"editStyleTitle": {
"message": "Уреди стил $stylename$",
"description": "Title of the page for editing styles",
"placeholders": {
"stylename": {
"content": "$1"
@ -133,55 +162,68 @@
}
},
"enableStyleLabel": {
"message": "Омогући"
"message": "Омогући",
"description": "Label for the button to enable a style"
},
"exportLabel": {
"message": "Извези"
"message": "Извези",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"genericAdd": {
"message": "Додај"
},
"genericEnabledLabel": {
"message": "Омогућено"
"findStylesForSite": {
"message": "Пронађи још стилова за овај сајт",
"description": "Text for a link that gets a list of styles for the current site"
},
"helpAlt": {
"message": "Помоћ"
"message": "Помоћ",
"description": "Alternate text for help buttons"
},
"helpKeyMapCommand": {
"message": "Укуцај име команде"
"message": "Укуцај име команде",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"helpKeyMapHotkey": {
"message": "Притисни пречицу"
"message": "Притисни пречицу",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"importAppendLabel": {
"message": "Додај стилу"
"message": "Додај стилу",
"description": "Label for the button to import a style and append to the existing sections"
},
"importAppendTooltip": {
"message": "Додај увезени стил тренутном стилу"
"message": "Додај увезени стил тренутном стилу",
"description": "Tooltip for the button to import a style and append to the existing sections"
},
"importLabel": {
"message": "Увези"
"message": "Увези",
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
},
"importReplaceLabel": {
"message": "Упиши преко стила"
"message": "Упиши преко стила",
"description": "Label for the button to import and overwrite current style"
},
"importReplaceTooltip": {
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил"
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил",
"description": "Label for the button to import and overwrite current style"
},
"installUpdate": {
"message": "Инсталирај ажурирање"
"message": "Инсталирај ажурирање",
"description": "Label for the button to install an update for a single style"
},
"linkGetHelp": {
"message": "Помоћ"
"message": "Помоћ",
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
},
"linkGetStyles": {
"message": "Преузмите стилове"
"message": "Преузмите стилове",
"description": "Help link text on the manage page e.g. https://userstyles.org"
},
"linterIssues": {
"message": "Проблеми"
"message": "Проблеми",
"description": "Label for the CSS linter issues block on the style edit page"
},
"linterIssuesHelp": {
"message": "Проблем пронађен од стране $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": {
"link": {
"content": "$1"
@ -189,85 +231,100 @@
}
},
"manageFilters": {
"message": "Филтери"
"message": "Филтери",
"description": "Label for filters container"
},
"manageHeading": {
"message": "Инсталирани стилови"
"message": "Инсталирани стилови",
"description": "Heading for the manage page"
},
"manageOnlyEnabled": {
"message": "Само омогућени стилови"
"message": "Само омогућени стилови",
"description": "Checkbox to show only enabled styles"
},
"menuShowBadge": {
"message": "Прикажи број активних стилова"
"message": "Прикажи број активних стилова",
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
},
"noStylesForSite": {
"message": "Нема инсталираних стилова за овај сајт."
"message": "Нема инсталираних стилова за овај сајт.",
"description": "Text displayed when no styles are installed for the current site"
},
"openManage": {
"message": "Управљај инсталираним стиловима"
},
"openOptions": {
"message": "Опције"
"message": "Управљај инсталираним стиловима",
"description": "Link to open the manage page."
},
"optionsHeading": {
"message": "Опције"
},
"optionsSyncUrl": {
"message": "УРЛ"
"message": "Опције",
"description": "Heading for options section on manage page."
},
"popupStylesFirst": {
"message": "Излистај стилове пре команди у менију дугмета на алатној траци"
"message": "Излистај стилове пре команди у менију дугмета на алатној траци",
"description": "Label for the checkbox controlling section order in the popup."
},
"prefShowBadge": {
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци"
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци",
"description": "Label for the checkbox controlling toolbar badge text."
},
"replace": {
"message": "Замени"
"message": "Замени",
"description": "Label before the replace input field in the editor shown on Ctrl-H"
},
"replaceAll": {
"message": "Замени све"
"message": "Замени све",
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
},
"replaceWith": {
"message": "Замени са"
"message": "Замени са",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"search": {
"message": "Претражи"
"message": "Претражи",
"description": "Label before the search input field in the editor shown on Ctrl-F"
},
"searchRegexp": {
"message": "Користи /re/ синтаксу за претрагу регуларним изразом"
"message": "Користи /re/ синтаксу за претрагу регуларним изразом",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"sectionAdd": {
"message": "Додај нови одељак"
"message": "Додај нови одељак",
"description": "Label for the button to add a section"
},
"sectionCode": {
"message": "Код"
"message": "Код",
"description": "Label for the code for a section"
},
"sectionRemove": {
"message": "Уклони одељак"
},
"sections": {
"message": "Одељци"
"message": "Уклони одељак",
"description": "Label for the button to remove a section"
},
"styleBadRegexp": {
"message": "Регуларни израз је неисправан."
"message": "Регуларни израз је неисправан.",
"description": "Validation message for a bad regexp in a style"
},
"styleBeautify": {
"message": "Улепшај"
"message": "Улепшај",
"description": "Label for the CSS-beautifier button on the edit style page"
},
"styleCancelEditLabel": {
"message": "Назад на управљање"
"message": "Назад на управљање",
"description": "Label for cancel button for style editing"
},
"styleChangesNotSaved": {
"message": "Направили сте измене овог стила које нисте сачували."
"message": "Направили сте измене овог стила које нисте сачували.",
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
},
"styleEnabledLabel": {
"message": "Омогућено"
"message": "Омогућено",
"description": "Label for the enabled state of styles"
},
"styleFromMozillaFormatPrompt": {
"message": "Налепи код у Mozilla формату"
"message": "Налепи код у Mozilla формату",
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
},
"styleInstall": {
"message": "Инсталирати '$stylename$' у Stylus?",
"description": "Confirmation when installing a style",
"placeholders": {
"stylename": {
"content": "$1"
@ -275,22 +332,28 @@
}
},
"styleMissingName": {
"message": "Унесите назив"
"message": "Унесите назив",
"description": "Error displayed when user saves without providing a name"
},
"styleMozillaFormatHeading": {
"message": "Mozilla формат"
"message": "Mozilla формат",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
},
"styleSaveLabel": {
"message": "Сачувај"
"message": "Сачувај",
"description": "Label for save button for style editing"
},
"styleToMozillaFormatHelp": {
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org."
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
},
"styleToMozillaFormatTitle": {
"message": "Стил у Mozilla формату"
"message": "Стил у Mozilla формату",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"styleUpdate": {
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
"description": "Confirmation when updating a style",
"placeholders": {
"stylename": {
"content": "$1"
@ -298,19 +361,24 @@
}
},
"stylusUnavailableForURL": {
"message": "Stylus не ради на страницама као што је ова."
"message": "Stylus не ради на страницама као што је ова.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"undo": {
"message": "Опозови"
"message": "Опозови",
"description": "Button label"
},
"undoGlobal": {
"message": "Опозови (свеобухватно)"
"message": "Опозови (свеобухватно)",
"description": "CSS-beautify global Undo button label"
},
"updateAllCheckSucceededNoUpdate": {
"message": "Сви стилови су ажурирани."
"message": "Сви стилови су ажурирани.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"updateCheckFailBadResponseCode": {
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": {
"code": {
"content": "$1"
@ -318,18 +386,23 @@
}
},
"updateCheckFailServerUnreachable": {
"message": "Ажурирање није успело: сервер није доступан."
"message": "Ажурирање није успело: сервер није доступан.",
"description": "Text that displays when an update check failed because the update server is unreachable"
},
"updateCheckSucceededNoUpdate": {
"message": "Стил је ажуриран."
"message": "Стил је ажуриран.",
"description": "Text that displays when an update check completed and no update is available"
},
"updateCompleted": {
"message": "Ажурирање је комплетирано."
"message": "Ажурирање је комплетирано.",
"description": "Text that displays when an update completed"
},
"writeStyleFor": {
"message": "Упиши стил за:"
"message": "Упиши стил за:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
},
"writeStyleForURL": {
"message": "ову УРЛ адресу"
"message": "ову УРЛ адресу",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,15 @@
{
"addStyleLabel": {
"message": "క్రొత్త స్టైల్ వ్రాయండి"
"message": "క్రొత్త స్టైల్ వ్రాయండి",
"description": "Label for the button to go to the add style page"
},
"appliesAdd": {
"message": "చేర్చు"
"message": "చేర్చు",
"description": "Label for the button to add an 'applies' entry"
},
"appliesDisplay": {
"message": "వేటికి వర్తిస్తుంది; $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": {
"applies": {
"content": "$1"
@ -14,54 +17,51 @@
}
},
"appliesDisplayTruncatedSuffix": {
"message": "ఇంకా మరిన్ని"
"message": "ఇంకా మరిన్ని",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesRemove": {
"message": "తొలగించు"
"message": "తొలగించు",
"description": "Label for the button to remove an 'applies' entry"
},
"appliesToEverything": {
"message": "అన్నిటికీ"
},
"confirmDelete": {
"message": "తొలగించు"
},
"confirmSave": {
"message": "భద్రపరచు"
"message": "అన్నిటికీ",
"description": "Text displayed for styles that apply to all sites"
},
"deleteStyleConfirm": {
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?"
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
"description": "Confirmation before deleting a style"
},
"deleteStyleLabel": {
"message": "తొలగించు"
"message": "తొలగించు",
"description": "Label for the button to delete a style"
},
"disableStyleLabel": {
"message": "అచేతనించు"
},
"editDeleteText": {
"message": "తొలగించు"
"message": "అచేతనించు",
"description": "Label for the button to disable a style"
},
"editStyleLabel": {
"message": "మార్చు"
"message": "మార్చు",
"description": "Label for the button to go to the edit style page"
},
"enableStyleLabel": {
"message": "చేతనించు"
},
"genericAdd": {
"message": "చేర్చు"
"message": "చేతనించు",
"description": "Label for the button to enable a style"
},
"helpAlt": {
"message": "సహాయం"
"message": "సహాయం",
"description": "Alternate text for help buttons"
},
"manageHeading": {
"message": "స్థాపిత శైలులు"
"message": "స్థాపిత శైలులు",
"description": "Heading for the manage page"
},
"manageTitle": {
"message": "స్టైలిష్"
},
"sections": {
"message": "విభాగాలు"
"message": "స్టైలిష్",
"description": "Title for the manage page"
},
"styleSaveLabel": {
"message": "భద్రపరచు"
"message": "భద్రపరచు",
"description": "Label for save button for style editing"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,285 +1 @@
{
"InaccessibleFileHint": {
"message": "Stylus не може отримати доступ до деяких типів файлів (наприклад, файли PDF і JSON)."
},
"addStyleLabel": {
"message": "Написати новий стиль"
},
"addStyleTitle": {
"message": "Додати стиль"
},
"alphaChannel": {
"message": "Прозорість"
},
"appliesAdd": {
"message": "Додати"
},
"appliesDisplay": {
"message": "Застосувати до $applies$",
"placeholders": {
"applies": {
"content": "$1"
}
}
},
"appliesDisplayTruncatedSuffix": {
"message": "та інші"
},
"appliesDomainOption": {
"message": "URL в домені"
},
"appliesHelp": {
"message": "Щоб вказати, до яких URL відноситься код в цьому розділі, скористайтеся параметром \"Застосувати до\"."
},
"appliesLabel": {
"message": "Застосувати до"
},
"appliesLineWidgetLabel": {
"message": "Показати цільові сайти секцій"
},
"appliesRemove": {
"message": "Видалити"
},
"appliesRemoveError": {
"message": "Не можна видалити останній елемент"
},
"appliesSpecify": {
"message": "Вказати"
},
"appliesToEverything": {
"message": "Все"
},
"appliesUrlPrefixOption": {
"message": "URL, що починаються з "
},
"applyAllUpdates": {
"message": "Застосувати всі оновлення"
},
"author": {
"message": "Автор"
},
"backupButtons": {
"message": "Резервне копіювання"
},
"bckpInstStyles": {
"message": "Експорт стилів"
},
"checkingForUpdate": {
"message": "Перевірка ... "
},
"clickToUninstall": {
"message": "Натисніть, щоб видалити "
},
"cm_selectByTokensTooltip": {
"message": "Приклади токенов: .foo-бар-2 #aabbcc 0,32 !important\nЯкщо вимкнено: вибираються слова, розділені розділовими знаками."
},
"confirmCancel": {
"message": "Скасувати "
},
"confirmDelete": {
"message": "Видалити "
},
"confirmNo": {
"message": "Ні"
},
"confirmSave": {
"message": "Зберегти"
},
"confirmStop": {
"message": "Зупинити"
},
"confirmYes": {
"message": "Так"
},
"deleteStyleLabel": {
"message": "Видалити"
},
"disableStyleLabel": {
"message": "Вимкнути"
},
"editDeleteText": {
"message": "Видалити"
},
"findStyles": {
"message": "Знайти стилі"
},
"genericAdd": {
"message": "Додати"
},
"genericDescription": {
"message": "Опис"
},
"genericDisabledLabel": {
"message": "Відключено"
},
"genericEnabledLabel": {
"message": "Увімкнено"
},
"genericError": {
"message": "Помилка"
},
"genericHistoryLabel": {
"message": "Історія"
},
"genericNext": {
"message": "Наступний "
},
"genericPrevious": {
"message": "Попередній"
},
"genericResetLabel": {
"message": "Скинути"
},
"genericSavedMessage": {
"message": "Збережено"
},
"genericTitle": {
"message": "Ім'я"
},
"genericUnknown": {
"message": "Невідомо"
},
"gettingStyles": {
"message": "Отримання всіх стилів ..."
},
"helpAlt": {
"message": "Довідка"
},
"helpKeyMapCommand": {
"message": "Введіть ім'я команди"
},
"helpKeyMapHotkey": {
"message": "Натисніть клавішу"
},
"hostDisabled": {
"message": "Цей хост вимкнено через помилку в поточній версії браузера, що використовується"
},
"importLabel": {
"message": "Імпорт"
},
"importReplaceLabel": {
"message": "Замінити стиль"
},
"importReportLegendIdentical": {
"message": "ідентичні пропущені"
},
"importReportLegendInvalid": {
"message": "недісних пропущено"
},
"importReportTitle": {
"message": "Імпорт стилів закінчено"
},
"installUpdate": {
"message": "Встановити оновлення"
},
"manageFavicons": {
"message": "Піктограми для цільових сайтів"
},
"manageFilters": {
"message": "Фільтри"
},
"manageHeading": {
"message": "Встановити Styles"
},
"manageNewUI": {
"message": "Новий макет інтерфейсу управління користувача"
},
"meta_unknownJSONLiteral": {
"message": "Невірний JSON: $literal$не є дійсним літералом JSON",
"placeholders": {
"literal": {
"content": "$1"
}
}
},
"openManage": {
"message": "Керування"
},
"openOptions": {
"message": "Налаштування"
},
"optionsHeading": {
"message": "Налаштування"
},
"optionsReset": {
"message": "Скидання налаштувань до значень за замовчуванням"
},
"optionsResetButton": {
"message": "Скинути параметри"
},
"optionsSubheading": {
"message": "Додатково"
},
"optionsSyncLogin": {
"message": "Логін"
},
"optionsSyncStatusRelogin": {
"message": "Сеанс закінчився, будь ласка, увійдіть ще раз."
},
"paginationNext": {
"message": "Наступна сторінка"
},
"paginationPrevious": {
"message": "Попередня сторінка"
},
"replace": {
"message": "Замінити"
},
"replaceAll": {
"message": "Замінити все"
},
"retrieveBckp": {
"message": "Імпорт стилів"
},
"search": {
"message": "Пошук"
},
"searchStylesAll": {
"message": "Усі"
},
"searchStylesCode": {
"message": "CSS код"
},
"searchStylesHelp": {
"message": "</> або <Ctrl-F>клавіша фокусує поле пошуку.\nРежим за замовчуванням — це пошук у звичайному тексті для всіх термінів, розділених пробілами, у будь-якому порядку.\nТочні слова: оберніть запит у подвійні лапки, напр. <.header ~ div\">\nРегулярні вирази: включають косі риски та прапорці, напр.</body.*?\\ba\\b/i>\n«By URL» в селекторі області: знаходить стилі, які застосовуються до повністю вказаної URL-адреси, напр. https://www.example.org/\n\"Metadata\" в селектрі області: шукає в іменах, \"applies to\" специфікаторів, URL-адреси встановлення, URL-адресі оновлення та всьому блоку метаданих для стилів CSS користувача."
},
"searchStylesName": {
"message": "Назва"
},
"sectionCode": {
"message": "Код"
},
"sections": {
"message": "Розділи"
},
"styleBeautify": {
"message": "Облагородити"
},
"styleCancelEditLabel": {
"message": "Повернутися до керування"
},
"styleEnabledLabel": {
"message": "Увімкнено"
},
"stylePreferSchemeLabel": {
"message": "Темна/Світла тема"
},
"styleSaveLabel": {
"message": "Зберегти"
},
"syncErrorRelogin": {
"message": "Помилка синхронізації. Ви вийшли із системи. \nСпробуйте повторно увійти до системи в налаштуваннях Stylus."
},
"toggleStyle": {
"message": "Включити/виключити стиль"
},
"writeStyleFor": {
"message": "Створити стиль для:"
},
"writeStyleForURL": {
"message": "цей URL"
},
"zipStyles": {
"message": "Запаковування стилів ... "
}
}
{}

View File

@ -1,379 +0,0 @@
{
"InaccessibleFileHint": {
"message": "Stylus không thể truy cập một số kiểu tập tin (chẳng hạn như PDF và JSON)."
},
"addStyleLabel": {
"message": "Viết bảng định kiểu mới"
},
"addStyleTitle": {
"message": "Thêm bảng định kiểu"
},
"alphaChannel": {
"message": "Độ mờ"
},
"appliesAdd": {
"message": "Thêm"
},
"appliesDisplay": {
"message": "Áp dụng với: $applies$",
"placeholders": {
"applies": {
"content": "$1"
}
}
},
"appliesDisplayTruncatedSuffix": {
"message": "và một số khác"
},
"appliesDomainOption": {
"message": "Các địa chỉ thuộc tên miền này"
},
"appliesHelp": {
"message": "Dùng tuỳ chọn \"Áp dụng với\" để giới hạn các địa chỉ cho đoạn mã này"
},
"appliesLabel": {
"message": "Áp dụng với"
},
"appliesLineWidgetLabel": {
"message": "Hiển thị thông tin \"Áp dụng với\""
},
"appliesLineWidgetWarning": {
"message": "Không hoạt động với CSS tối giản"
},
"appliesRegexpOption": {
"message": "URL khớp với biểu thức chính quy"
},
"appliesRemove": {
"message": "Xoá"
},
"appliesRemoveError": {
"message": "Không thể xoá mục \"Áp dụng với\" cuối cùng"
},
"appliesSpecify": {
"message": "Chỉ định"
},
"appliesToEverything": {
"message": "Tất cả"
},
"appliesUrlPrefixOption": {
"message": "URL bắt đầu bằng"
},
"author": {
"message": "Tác giả"
},
"backupButtons": {
"message": "Sao lưu"
},
"backupMessage": {
"message": "Để nhập tập tin sao lưu, kéo và thả nó vào trang này hoặc nhấp vào nút Nhập.\n\nĐể xuất một bản sao lưu tương thích với Stylus trước phiên bản 1.5.18, nhấp chuột phải hoặc nhấn giữ Shift khi nhấp chuột trái vào nút Xuất."
},
"bckpInstStyles": {
"message": "Xuất bảng định kiểu"
},
"checkForUpdate": {
"message": "Kiểm tra bản cập nhật mới"
},
"checkingForUpdate": {
"message": "Đang kiểm tra..."
},
"clickToUninstall": {
"message": "Nhấp để huỷ kích hoạt"
},
"cm_autoCloseBrackets": {
"message": "Tự động đóng ngoặc và nháy"
},
"cm_autoCloseBracketsTooltip": {
"message": "Tự động thêm dấu đóng tương ứng khi nhập một trong các dấu (, [, {, ' và \"."
},
"cm_autocompleteOnTyping": {
"message": "Tự động hoàn thành"
},
"cm_colorpicker": {
"message": "Bộ chọn màu cho màu CSS"
},
"cm_indentWithTabs": {
"message": "Lùi đầu dòng thông minh bằng tab"
},
"cm_lineWrapping": {
"message": "Gập dòng dài"
},
"cm_linter": {
"message": "Trình phân tích cú pháp"
},
"cm_matchHighlight": {
"message": "Làm nổi"
},
"cm_matchHighlightSelection": {
"message": "Chỉ vùng được chọn"
},
"cm_matchHighlightToken": {
"message": "Token nằm dưới con trỏ văn bản"
},
"cm_selectByTokens": {
"message": "Nhấp đúp để chọn token"
},
"cm_selectByTokensTooltip": {
"message": "Ví dụ về token: .foo-bar-2 #aabbcc 0.32 !important\nKhi tắt: Chọn từ (phân tách bằng dấu câu)."
},
"cm_smartIndent": {
"message": "Lùi đầu dòng thông minh"
},
"cm_tabSize": {
"message": "Chiều rộng tab"
},
"cm_theme": {
"message": "Chủ đề"
},
"colorpickerSwitchFormatTooltip": {
"message": "Đổi định dạng: HEX → RGB → HSL.\nNhấn giữ phím Shift khi nhấp để đảo thứ tự.\nPhím tắt: PgUp và PgDn."
},
"colorpickerTooltip": {
"message": "Mở bộ chọn màu"
},
"configOnChange": {
"message": "khi thay đổi"
},
"configOnChangeTooltip": {
"message": "Tự động lưu và áp dụng"
},
"configureStyle": {
"message": "Thiết lập"
},
"configureStyleOnHomepage": {
"message": "Thiết lập trên trang chủ"
},
"confirmCancel": {
"message": "Huỷ"
},
"confirmClose": {
"message": "Đóng"
},
"confirmDefault": {
"message": "Dùng mặc định"
},
"confirmDelete": {
"message": "Xoá"
},
"confirmDiscardChanges": {
"message": "Huỷ thay đổi?"
},
"confirmNo": {
"message": "Không"
},
"confirmSave": {
"message": "Lưu"
},
"confirmStop": {
"message": "Dừng"
},
"confirmYes": {
"message": "Có"
},
"connectingDropbox": {
"message": "Đang kết nối với Dropbox..."
},
"connectingDropboxNotAllowed": {
"message": "Tính năng kết nối với Dropbox chỉ khả dụng khi cài ứng dụng trực tiếp từ cửa hàng web"
},
"copied": {
"message": "Đã chép vào khay nhớ tạm"
},
"copy": {
"message": "Chép vào khay nhớ tạm"
},
"dateAbbrDay": {
"message": "$value$ ngày",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrHour": {
"message": "$value$ giờ",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrMonth": {
"message": "$value$ tháng",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrYear": {
"message": "$value$ năm",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateInstalled": {
"message": "Ngày cài đặt"
},
"dateUpdated": {
"message": "Ngày cập nhật"
},
"defaultTheme": {
"message": "mặc định"
},
"deleteStyleConfirm": {
"message": "Bạn có chắc chắn muốn xoá bảng định kiểu này không?"
},
"deleteStyleLabel": {
"message": "Xoá"
},
"disableAllStyles": {
"message": "Tắt tất cả bảng định kiểu"
},
"disableAllStylesOff": {
"message": "Bật tất cả bảng định kiểu"
},
"disableStyleLabel": {
"message": "Vô hiệu hoá"
},
"draftAction": {
"message": "Chọn \"Có\" để tải bản nháp hoặc \"Không\" để xoá nó đi."
},
"editDeleteText": {
"message": "Xoá"
},
"editGotoLine": {
"message": "Đi đến dòng (hoặc dòng:cột)"
},
"editStyleHeading": {
"message": "Sửa bảng định kiểu"
},
"editStyleLabel": {
"message": "Sửa"
},
"editStyleTitle": {
"message": "Sửa bảng định kiểu $stylename$",
"placeholders": {
"stylename": {
"content": "$1"
}
}
},
"editorSettings": {
"message": "Cài đặt trình soạn thảo"
},
"enableStyleLabel": {
"message": "Kích hoạt"
},
"exportCompatible": {
"message": "Xuất (chế độ tương thích)"
},
"exportLabel": {
"message": "Xuất"
},
"exportSavedSuccess": {
"message": "Lưu thành công"
},
"externalFeedback": {
"message": "Phản hồi"
},
"externalHomepage": {
"message": "Trang chủ"
},
"externalLink": {
"message": "Liên kết ngoài"
},
"externalSupport": {
"message": "Ủng hộ"
},
"genericAdd": {
"message": "Thêm"
},
"genericDescription": {
"message": "Mô tả"
},
"genericDisabledLabel": {
"message": "Vô hiệu hoá"
},
"genericEnabledLabel": {
"message": "Kích hoạt"
},
"genericError": {
"message": "Lỗi"
},
"genericHistoryLabel": {
"message": "Lịch sử"
},
"genericNext": {
"message": "Sau"
},
"genericPrevious": {
"message": "Trước"
},
"genericResetLabel": {
"message": "Đặt lại"
},
"genericSavedMessage": {
"message": "Đã lưu"
},
"genericTest": {
"message": "Thứ"
},
"genericTitle": {
"message": "Tiêu đề"
},
"genericUnknown": {
"message": "Không xác định"
},
"helpAlt": {
"message": "Trợ giúp"
},
"importLabel": {
"message": "Nhập"
},
"importReportLegendAdded": {
"message": "đã thêm"
},
"importReportLegendIdentical": {
"message": "Đã bỏ qua các tệp trùng lặp"
},
"importReportLegendInvalid": {
"message": "Đã bỏ qua các tệp không hợp lệ"
},
"importReportLegendUpdatedBoth": {
"message": "đã cập nhật siêu thông tin và mã"
},
"importReportLegendUpdatedCode": {
"message": "đã cập nhật mã"
},
"importReportLegendUpdatedMeta": {
"message": "đã cập nhật siêu thông tin"
},
"importReportTitle": {
"message": "Đã nhập xong"
},
"installUpdateFromLabel": {
"message": "Kiểm tra bản cập nhật mới"
},
"license": {
"message": "Giấy phép"
},
"linterCSSLintIncompatible": {
"message": "CSSLint không hỗ trợ bộ tiền xử lý $preprocessorname$",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"linterJSONError": {
"message": "Định dạng JSON không hợp lệ"
},
"styleEnabledLabel": {
"message": "Kích hoạt"
},
"styleSaveLabel": {
"message": "Lưu"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,41 +6,35 @@
/* global syncMan */
/* global updateMan */
/* global usercssMan */
/* global usoApi */
/* global uswApi */
/* global FIREFOX UA activateTab openURL */ // toolbox.js
/* global colorScheme */ // color-scheme.js
/* global
FIREFOX
URLS
activateTab
download
findExistingTab
getActiveTab
isTabReplaceable
openURL
*/ // toolbox.js
'use strict';
//#region API
addAPI(/** @namespace API */ {
/** Temporary storage for data needed elsewhere e.g. in a content script */
data: ((data = {}) => ({
del: key => delete data[key],
get: key => data[key],
has: key => key in data,
pop: key => {
const val = data[key];
delete data[key];
return val;
},
set: (key, val) => {
data[key] = val;
},
}))(),
styles: styleMan,
sync: syncMan,
updater: updateMan,
usercss: usercssMan,
uso: usoApi,
usw: uswApi,
colorScheme,
/** @type {BackgroundWorker} */
worker: createWorker({url: '/background/background-worker'}),
download(url, opts) {
return typeof url === 'string' && url.startsWith(URLS.uso) &&
this.sender.url.startsWith(URLS.uso) &&
download(url, opts || {});
},
/** @returns {string} */
getTabUrlPrefix() {
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
@ -58,24 +52,10 @@ addAPI(/** @namespace API */ {
async openEditor(params) {
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
const wnd = chrome.windows && prefs.get('openEditInWindow');
const 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
if (wndPos) {
const {left, top, width, height} = wndPos;
const r = left + width;
const b = top + height;
const peek = 32;
if (isNaN(r) || r < peek || left > screen.availWidth - peek || width < 100) {
delete wndPos.left;
delete wndPos.width;
}
if (isNaN(b) || b < peek || top > screen.availHeight - peek || height < 100) {
delete wndPos.top;
delete wndPos.height;
}
}
const tab = await openURL({
url: `${u}`,
currentWindow: null,
@ -87,25 +67,30 @@ addAPI(/** @namespace API */ {
/** @returns {Promise<chrome.tabs.Tab>} */
async openManage({options = false, search, searchMode} = {}) {
const setUrlParams = url => {
const u = new URL(url);
if (search) u.searchParams.set('search', search);
if (searchMode) u.searchParams.set('searchMode', searchMode);
if (options) u.hash = '#stylus-options';
return u.href;
};
const base = chrome.runtime.getURL('manage.html');
const url = setUrlParams(base);
const tabs = await browser.tabs.query({url: base + '*'});
const same = tabs.find(t => t.url === url);
let tab = same || tabs[0];
if (!tab) {
API.prefsDb.get('badFavs'); // prime the cache to avoid flicker/delay when opening the page
tab = await openURL({url, newTab: true});
} else if (!same) {
msg.sendTab(tab.id, {method: 'pushState', url: setUrlParams(tab.url)});
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
}
return activateTab(tab); // activateTab unminimizes the window
if (options) {
url += '#stylus-options';
}
let 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;
}
tab = await getActiveTab();
return isTabReplaceable(tab, url)
? activateTab(tab, {url})
: browser.tabs.create({url}).then(activateTab); // activateTab unminimizes the window
},
/**
@ -158,25 +143,10 @@ if (chrome.commands) {
}
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
if (reason === 'install') {
if (UA.mobile) prefs.set('manage.newUI', false);
if (UA.windows) prefs.set('editor.keyMap', 'sublime');
}
// TODO: remove this before 1.5.23 as it's only for a few users who installed git 26b75e77
if (reason === 'update' && previousVersion === '1.5.22') {
for (const dbName of ['drafts', prefs.STORAGE_KEY]) {
try {
indexedDB.open(dbName).onsuccess = async e => {
const idb = /** @type IDBDatabase */ e.target.result;
const ta = idb.objectStoreNames[0] === 'data' && idb.transaction(['data']);
if (ta && ta.objectStore('data').autoIncrement) {
ta.abort();
idb.close();
await new Promise(setTimeout);
indexedDB.deleteDatabase(dbName);
}
};
} catch (e) {}
if (reason === 'update') {
const [a, b, c] = (previousVersion || '').split('.');
if (a <= 1 && b <= 5 && c <= 13) { // 1.5.13
require(['/background/remove-unused-storage']);
}
}
});
@ -193,8 +163,6 @@ msg.on((msg, sender) => {
//#endregion
Promise.all([
browser.extension.isAllowedFileSchemeAccess()
.then(res => API.data.set('hasFileAccess', res)),
bgReady.styles,
/* These are loaded conditionally.
Each item uses `require` individually so IDE can jump to the source and track usage. */
@ -208,6 +176,6 @@ Promise.all([
require(['/background/context-menus']),
]).then(() => {
bgReady._resolveAll();
msg.ready = true;
msg.isBgReady = true;
msg.broadcast({method: 'backgroundReady'});
});

View File

@ -1,91 +0,0 @@
/* global prefs */
/* exported colorScheme */
'use strict';
const colorScheme = (() => {
const changeListeners = new Set();
const kSTATE = 'schemeSwitcher.enabled';
const kSTART = 'schemeSwitcher.nightStart';
const kEND = 'schemeSwitcher.nightEnd';
const SCHEMES = ['dark', 'light'];
const isDark = {
never: null,
dark: true,
light: false,
system: false,
time: false,
};
let isDarkNow = false;
prefs.subscribe(kSTATE, () => update());
prefs.subscribe([kSTART, kEND], (key, value) => {
updateTimePreferDark();
createAlarm(key, value);
}, {runNow: true});
chrome.alarms.onAlarm.addListener(({name}) => {
if (name === kSTART || name === kEND) {
updateTimePreferDark();
}
});
return {
SCHEMES,
onChange(listener) {
changeListeners.add(listener);
},
isDark: () => isDarkNow,
/** @param {StyleObj} _ */
shouldIncludeStyle({preferScheme: ps}) {
return prefs.get(kSTATE) === 'never' ||
!SCHEMES.includes(ps) ||
isDarkNow === (ps === 'dark');
},
updateSystemPreferDark(val) {
update('system', val);
return true;
},
};
function calcTime(key) {
const [h, m] = prefs.get(key).split(':');
return (h * 3600 + m * 60) * 1000;
}
function createAlarm(key, value) {
const date = new Date();
const [h, m] = value.split(':');
date.setHours(h, m, 0, 0);
if (date.getTime() < Date.now()) {
date.setDate(date.getDate() + 1);
}
chrome.alarms.create(key, {
when: date.getTime(),
periodInMinutes: 24 * 60,
});
}
function updateTimePreferDark() {
const now = Date.now() - new Date().setHours(0, 0, 0, 0);
const start = calcTime(kSTART);
const end = calcTime(kEND);
const val = start > end ?
now >= start || now < end :
now >= start && now < end;
update('time', val);
}
function update(type, val) {
if (type) {
if (isDark[type] === val) return;
isDark[type] = val;
}
val = isDark[prefs.get(kSTATE)];
if (isDarkNow !== val) {
isDarkNow = val;
for (const listener of changeListeners) {
listener(isDarkNow);
}
}
}
})();

View File

@ -5,19 +5,16 @@
* Common stuff that's loaded first so it's immediately available to all background scripts
*/
window.bgReady = {}; /* global bgReady */
/* exported
addAPI
bgReady
compareRevision
*/
const bgReady = {};
bgReady.styles = new Promise(r => (bgReady._resolveStyles = r));
bgReady.all = new Promise(r => (bgReady._resolveAll = r));
const uuidIndex = Object.assign(new Map(), {
custom: {},
/** `obj` must have a unique `id`, a UUIDv4 `_id`, and Date.now() for `_rev`. */
addCustomId(obj, {get = () => obj, set}) {
Object.defineProperty(uuidIndex.custom, obj.id, {get, set});
},
});
/* exported addAPI */
function addAPI(methods) {
for (const [key, val] of Object.entries(methods)) {
const old = API[key];
@ -29,64 +26,6 @@ function addAPI(methods) {
}
}
/* exported createCache */
/** Creates a FIFO limit-size map. */
function createCache({size = 1000, onDeleted} = {}) {
const map = new Map();
const buffer = Array(size);
let index = 0;
let lastIndex = 0;
return {
get(id) {
const item = map.get(id);
return item && item.data;
},
set(id, data) {
if (map.size === size) {
// full
map.delete(buffer[lastIndex].id);
if (onDeleted) {
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
}
lastIndex = (lastIndex + 1) % size;
}
const item = {id, data, index};
map.set(id, item);
buffer[index] = item;
index = (index + 1) % size;
},
delete(id) {
const item = map.get(id);
if (!item) {
return false;
}
map.delete(item.id);
const lastItem = buffer[lastIndex];
lastItem.index = item.index;
buffer[item.index] = lastItem;
lastIndex = (lastIndex + 1) % size;
if (onDeleted) {
onDeleted(item.id, item.data);
}
return true;
},
clear() {
map.clear();
index = lastIndex = 0;
},
has: id => map.has(id),
*entries() {
for (const [id, item] of map) {
yield [id, item.data];
}
},
*values() {
for (const item of map.values()) {
yield item.data;
}
},
get size() {
return map.size;
},
};
function compareRevision(rev1, rev2) {
return rev1 - rev2;
}

View File

@ -1,61 +1,80 @@
/* global browserCommands */// background.js
/* global msg */
/* global prefs */
/* global CHROME URLS ignoreChromeError */// toolbox.js
/* global CHROME FIREFOX URLS ignoreChromeError */// toolbox.js
'use strict';
chrome.management.getSelf(ext => {
const contextMenus = Object.assign({
(() => {
const contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: togglePref,
click: info => prefs.set(info.menuItemId, info.checked),
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'optionsOpenManager',
title: 'openStylesManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
}, ext.installType === 'development' && {
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
}, CHROME && {
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + '*'],
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);
createContextMenus(Object.keys(contextMenus));
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
function createContextMenus(ids) {
async function createContextMenus(ids) {
for (const id of ids) {
const item = Object.assign({id, contexts: ['browser_action']}, contextMenus[id]);
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 (typeof prefs.defaults[id] === 'boolean') {
if (item.type) {
prefs.subscribe(id, togglePresence);
} else {
item.type = 'checkbox';
item.checked = prefs.get(id);
prefs.subscribe(id, CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
}
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);
@ -72,11 +91,6 @@ chrome.management.getSelf(ext => {
createContextMenus([id]);
}
/** @param {chrome.contextMenus.OnClickData} info */
function togglePref(info) {
prefs.set(info.menuItemId, info.checked);
}
function togglePresence(id, checked) {
if (checked) {
createContextMenus([id]);
@ -84,4 +98,4 @@ chrome.management.getSelf(ext => {
chrome.contextMenus.remove(id, ignoreChromeError);
}
}
});
})();

View File

@ -2,17 +2,17 @@
'use strict';
/* exported createChromeStorageDB */
function createChromeStorageDB(PREFIX) {
function createChromeStorageDB() {
let INC;
const isMain = !PREFIX;
if (!PREFIX) PREFIX = 'style-';
return {
const PREFIX = 'style-';
const METHODS = {
delete(id) {
return chromeLocal.remove(PREFIX + id);
},
// FIXME: we don't use this method at all. Should we remove this?
get(id) {
return chromeLocal.getValue(PREFIX + id);
},
@ -21,9 +21,7 @@ function createChromeStorageDB(PREFIX) {
const all = await chromeLocal.get();
if (!INC) prepareInc(all);
return Object.entries(all)
.map(([key, val]) => key.startsWith(PREFIX) &&
(!isMain || Number(key.slice(PREFIX.length))) &&
val)
.map(([key, val]) => key.startsWith(PREFIX) && Number(key.slice(PREFIX.length)) && val)
.filter(Boolean);
},
@ -61,4 +59,8 @@ function createChromeStorageDB(PREFIX) {
}
}
}
return function dbExecChromeStorage(method, ...args) {
return METHODS[method](...args);
};
}

View File

@ -1,8 +1,5 @@
/* global addAPI */// common.js
/* global chromeLocal */// storage-util.js
/* global cloneError */// worker-util.js
/* global deepCopy */// toolbox.js
/* global prefs */
'use strict';
/*
@ -14,49 +11,16 @@
/* exported db */
const db = (() => {
let exec = async (...args) => (
exec = await tryUsingIndexedDB().catch(useChromeStorage)
)(...args);
const DB = 'stylish';
const DATABASE = 'stylish';
const STORE = 'styles';
const FALLBACK = 'dbInChromeStorage';
const ID_AS_KEY = {[DB]: true};
const getStoreName = dbName => dbName === DB ? 'styles' : 'data';
const cache = {};
const proxies = {};
const proxyHandler = {
get: ({dbName}, cmd) =>
(...args) =>
(dbName === DB ? exec : cachedExec)(dbName, cmd, ...args),
const dbApi = {
async exec(...args) {
dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
return dbApi.exec(...args);
},
};
/**
* @param {string} dbName
* @return {IDBObjectStore | {putMany: function(items:?[]):Promise<?[]>}}
*/
const getProxy = dbName => proxies[dbName] || (
(proxies[dbName] = new Proxy({dbName}, proxyHandler))
);
addAPI(/** @namespace API */ {
drafts: getProxy('drafts'),
/** Storage for big items that may exceed 8kB limit of chrome.storage.sync.
* To make an item syncable register it with uuidIndex.addCustomId. */
prefsDb: getProxy(prefs.STORAGE_KEY),
});
return {
styles: getProxy(DB),
};
async function cachedExec(dbName, cmd, a, b) {
const hub = cache[dbName] || (cache[dbName] = {});
const res = cmd === 'get' && a in hub ? hub[a] : await exec(...arguments);
if (cmd === 'get') {
hub[a] = deepCopy(res);
} else if (cmd === 'put') {
hub[ID_AS_KEY[dbName] ? a.id : b] = deepCopy(a);
} else if (cmd === 'delete') {
delete hub[a];
}
return res;
}
return dbApi;
async function tryUsingIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
@ -76,9 +40,9 @@ const db = (() => {
async function testDB() {
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
await dbExecIndexedDB(DB, 'put', {id});
const e = await dbExecIndexedDB(DB, 'get', id);
await dbExecIndexedDB(DB, 'delete', e.id); // throws if `e` or id is null
await dbExecIndexedDB('put', {id});
const e = await dbExecIndexedDB('get', id);
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
}
async function useChromeStorage(err) {
@ -88,18 +52,12 @@ const db = (() => {
console.warn('Failed to access indexedDB. Switched to storage API.', err);
}
await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
const BASES = {};
return (dbName, method, ...args) => (
BASES[dbName] || (
BASES[dbName] = createChromeStorageDB(dbName !== DB && `${dbName}-`)
)
)[method](...args);
return createChromeStorageDB();
}
async function dbExecIndexedDB(dbName, method, ...args) {
async function dbExecIndexedDB(method, ...args) {
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const storeName = getStoreName(dbName);
const store = (await open(dbName)).transaction([storeName], mode).objectStore(storeName);
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
const fn = method === 'putMany' ? putMany : storeRequest;
return fn(store, method, ...args);
}
@ -117,34 +75,21 @@ const db = (() => {
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
}
function open(name) {
function open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, 2);
request.onsuccess = e => resolve(create(e));
const request = indexedDB.open(DATABASE, 2);
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
request.onupgradeneeded = create;
});
}
function create(event) {
/** @type IDBDatabase */
const idb = event.target.result;
const dbName = idb.name;
const sn = getStoreName(dbName);
if (!idb.objectStoreNames.contains(sn)) {
if (event.type === 'success') {
idb.close();
return new Promise(resolve => {
indexedDB.deleteDatabase(dbName).onsuccess = () => {
resolve(open(dbName));
};
});
}
idb.createObjectStore(sn, ID_AS_KEY[dbName] ? {
if (event.oldVersion === 0) {
event.target.result.createObjectStore(STORE, {
keyPath: 'id',
autoIncrement: true,
} : undefined);
});
}
return idb;
}
})();

View File

@ -1,22 +1,16 @@
/* global API */// msg.js
/* global addAPI bgReady */// common.js
/* global colorScheme */
/* global prefs */
/* global tabMan */
/* global CHROME FIREFOX UA debounce ignoreChromeError */// toolbox.js
/* global CHROME FIREFOX VIVALDI debounce ignoreChromeError */// toolbox.js
'use strict';
/* exported iconMan */
const iconMan = (() => {
const ICON_SIZES = FIREFOX || CHROME && !UA.vivaldi ? [16, 32] : [19, 38];
(() => {
const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
const staleBadges = new Set();
const imageDataCache = new Map();
const badgeOvr = {color: '', text: ''};
// https://github.com/openstyles/stylus/issues/1287 Fenix can't use custom ImageData
const FIREFOX_ANDROID = FIREFOX && UA.mobile;
let isDark;
// https://github.com/openstyles/stylus/issues/335
let hasCanvas = FIREFOX_ANDROID ? false : loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
let hasCanvas = loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
.then(({data}) => (hasCanvas = data.some(b => b !== 255)));
addAPI(/** @namespace API */ {
@ -38,17 +32,13 @@ const iconMan = (() => {
chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
if (!frameId) tabMan.set(tabId, 'styleIds', undefined);
});
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'iframe') {
port.onDisconnect.addListener(onPortDisconnected);
}
});
colorScheme.onChange(val => {
isDark = val;
if (prefs.get('iconset') === -1) {
debounce(refreshAllIcons);
}
});
bgReady.all.then(() => {
prefs.subscribe([
'disableAll',
@ -64,29 +54,6 @@ const iconMan = (() => {
], () => debounce(refreshAllIcons), {runNow: true});
});
return {
/** Calling with no params clears the override */
overrideBadge({text = '', color = '', title = ''} = {}) {
if (badgeOvr.text === text) {
return;
}
badgeOvr.text = text;
badgeOvr.color = color;
refreshIconBadgeColor();
setBadgeText({text});
for (const tabId of tabMan.list()) {
if (text) {
setBadgeText({tabId, text});
} else {
refreshIconBadgeText(tabId);
}
}
chrome.browserAction.setTitle({
title: title && chrome.i18n.getMessage(title) || title || '',
});
},
};
function onPortDisconnected({sender}) {
if (tabMan.get(sender.tab.id, 'styleIds')) {
API.updateIconBadge.call({sender}, [], {lazyBadge: true});
@ -94,16 +61,14 @@ const iconMan = (() => {
}
function refreshIconBadgeText(tabId) {
if (badgeOvr.text) return;
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
setBadgeText({tabId, text});
}
function getIconName(hasStyles = false) {
const i = prefs.get('iconset');
const prefix = i === 0 || i === -1 && isDark ? '' : 'light/';
const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
return `${prefix}$SIZE$${postfix}`;
return `${iconset}$SIZE$${postfix}`;
}
function refreshIcon(tabId, force = false) {
@ -141,7 +106,7 @@ const iconMan = (() => {
// Caches imageData for icon paths
async function loadImage(url) {
const {OffscreenCanvas} = !FIREFOX && self.createImageBitmap && self || {};
const {OffscreenCanvas} = self.createImageBitmap && self || {};
const img = OffscreenCanvas
? await createImageBitmap(await (await fetch(url)).blob())
: await new Promise((resolve, reject) =>
@ -168,9 +133,9 @@ const iconMan = (() => {
}
function refreshIconBadgeColor() {
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
setBadgeBackgroundColor({
color: badgeOvr.color ||
prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'),
color,
});
}

View File

@ -42,9 +42,8 @@ const navMan = (() => {
/** @this {string} type */
function onFakeNavigation(data) {
const {url, frameId} = data;
onNavigation.call(this, data);
msg.sendTab(data.tabId, {method: 'urlChanged', url}, {frameId})
msg.sendTab(data.tabId, {method: 'urlChanged'}, {frameId: data.frameId})
.catch(msg.ignoreError);
}
})();
@ -66,21 +65,6 @@ bgReady.all.then(() => {
{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
*/

View File

@ -0,0 +1,103 @@
/* global addAPI */// common.js
'use strict';
/* CURRENTLY UNUSED */
(() => {
// 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 = async ({id}, queryString) => {
const query = gql(queryString);
return (await fetch(api, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
}),
body: query({
id,
}),
})).json();
};
addAPI(/** @namespace- API */ { // TODO: remove "-" when this is implemented
/**
* 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
}
}
`),
});
})();

View 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);

View File

@ -1,12 +1,10 @@
/* global API msg */// msg.js
/* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global bgReady createCache uuidIndex */// common.js
/* global calcStyleDigest styleCodeEmpty */// sections-util.js
/* global URLS stringAsRegExp tryRegExp */// toolbox.js
/* global bgReady compareRevision */// common.js
/* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js
/* global db */
/* global prefs */
/* global tabMan */
/* global usercssMan */
/* global colorScheme */
'use strict';
/*
@ -18,26 +16,18 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See livePreview in /edit.
*/
const styleUtil = {};
/* exported styleMan */
const styleMan = (() => {
Object.assign(styleUtil, {
id2style,
handleSave,
uuid2style,
});
//#region Declarations
/** @typedef {{
style: StyleObj,
preview?: StyleObj,
appliesTo: Set<string>,
style: StyleObj
preview?: StyleObj
appliesTo: Set<string>
}} StyleMapData */
/** @type {Map<number,StyleMapData>} */
const dataMap = new Map();
const uuidIndex = new Map();
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
const cachedStyleForUrl = createCache({
@ -52,69 +42,16 @@ const styleMan = (() => {
const compileRe = createCompiler(text => `^(${text})$`);
const compileSloppyRe = createCompiler(text => `^${text}$`);
const compileExclusion = createCompiler(buildExclusion);
const uuidv4 = crypto.randomUUID ? crypto.randomUUID.bind(crypto) : (() => {
const seeds = crypto.getRandomValues(new Uint16Array(8));
// 00001111-2222-M333-N444-555566667777
seeds[3] = seeds[3] & 0x0FFF | 0x4000; // UUID version 4, M = 4
seeds[4] = seeds[4] & 0x3FFF | 0x8000; // UUID variant 1, N = 8..0xB
return Array.from(seeds, hex4dashed).join('');
});
const MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
const INJ_ORDER = 'injectionOrder';
const order = {main: {}, prio: {}};
const orderWrap = {
id: INJ_ORDER,
value: mapObj(order, () => []),
_id: `${chrome.runtime.id}-${INJ_ORDER}`,
_rev: 0,
};
uuidIndex.addCustomId(orderWrap, {set: setOrder});
class MatchQuery {
constructor(url) {
this.url = url;
}
get urlWithoutHash() {
return this._set('urlWithoutHash', this.url.split('#', 1)[0]);
}
get urlWithoutParams() {
return this._set('urlWithoutParams', this.url.split(/[?#]/, 1)[0]);
}
get domain() {
return this._set('domain', tryURL(this.url).hostname);
}
get isOwnPage() {
return this._set('isOwnPage', this.url.startsWith(URLS.ownOrigin));
}
_set(name, value) {
Object.defineProperty(this, name, {value});
return value;
}
}
const DELETE_IF_NULL = ['id', 'customName'];
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = Promise.all([init(), prefs.ready]);
let ready = init();
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'livePreview') {
handleLivePreview(port);
} else if (port.name.startsWith('draft:')) {
handleDraft(port);
}
});
colorScheme.onChange(value => {
msg.broadcastExtension({method: 'colorScheme', value});
for (const {style} of dataMap.values()) {
if (colorScheme.SCHEMES.includes(style.preferScheme)) {
broadcastStyleUpdated(style, 'colorScheme');
}
}
});
chrome.runtime.onConnect.addListener(handleLivePreview);
//#endregion
//#region Exports
@ -124,28 +61,17 @@ const styleMan = (() => {
/** @returns {Promise<number>} style id */
async delete(id, reason) {
if (ready.then) await ready;
const {style, appliesTo} = dataMap.get(id);
const sync = reason !== 'sync';
const uuid = style._id;
db.styles.delete(id);
if (sync) API.sync.delete(uuid, Date.now());
for (const url of appliesTo) {
const data = id2data(id);
await db.exec('delete', id);
if (reason !== 'sync') {
API.sync.delete(data.style._id, Date.now());
}
for (const url of data.appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id];
}
dataMap.delete(id);
uuidIndex.delete(uuid);
mapObj(orderWrap.value, (group, type) => {
delete order[type][id];
const i = group.indexOf(uuid);
if (i >= 0) group.splice(i, 1);
});
setOrder(orderWrap, {calc: false});
if (style._usw && style._usw.token) {
// Must be called after the style is deleted from dataMap
API.usw.revoke(id);
}
API.drafts.delete(id);
uuidIndex.delete(data.style._id);
await msg.broadcast({
method: 'styleDeleted',
style: {id},
@ -153,23 +79,32 @@ const styleMan = (() => {
return id;
},
/** @returns {Promise<number>} style id */
async deleteByUUID(_id, rev) {
if (ready.then) await ready;
const id = uuidIndex.get(_id);
const oldDoc = id && id2style(id);
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return styleMan.delete(id, 'sync');
}
},
/** @returns {Promise<StyleObj>} */
async editSave(style) {
if (ready.then) await ready;
style = mergeWithMapped(style);
style.updateDate = Date.now();
return saveStyle(style, {reason: 'editSave'});
return handleSave(await saveStyle(style), 'editSave');
},
/** @returns {Promise<?StyleObj>} */
async find(...filters) {
async find(filter) {
if (ready.then) await ready;
for (const filter of filters) {
const filterEntries = Object.entries(filter);
for (const {style} of dataMap.values()) {
if (filterEntries.every(([key, val]) => style[key] === val)) {
return style;
}
const filterEntries = Object.entries(filter);
for (const {style} of dataMap.values()) {
if (filterEntries.every(([key, val]) => style[key] === val)) {
return style;
}
}
return null;
@ -178,58 +113,25 @@ const styleMan = (() => {
/** @returns {Promise<StyleObj[]>} */
async getAll() {
if (ready.then) await ready;
return getAllAsArray();
return Array.from(dataMap.values(), data2style);
},
/** @returns {Promise<Object<string,StyleObj[]>>}>} */
async getAllOrdered(keys) {
/** @returns {Promise<StyleObj>} */
async getByUUID(uuid) {
if (ready.then) await ready;
const res = mapObj(orderWrap.value, group => group.map(uuid2style).filter(Boolean));
if (res.main.length + res.prio.length < dataMap.size) {
for (const {style} of dataMap.values()) {
if (!(style.id in order.main) && !(style.id in order.prio)) {
res.main.push(style);
}
}
}
return keys
? mapObj(res, group => group.map(style => mapObj(style, null, keys)))
: res;
},
getOrder: () => orderWrap.value,
/** @returns {Promise<string | {[remoteId:string]: styleId}>}>} */
async getRemoteInfo(id) {
if (ready.then) await ready;
if (id) return calcRemoteId(id2style(id));
const res = {};
for (const {style} of dataMap.values()) {
const [rid, vars] = calcRemoteId(style);
if (rid) res[rid] = [style.id, vars];
}
return res;
return id2style(uuidIndex.get(uuid));
},
/** @returns {Promise<StyleSectionsToApply>} */
async getSectionsByUrl(url, id, isInitialApply) {
if (ready.then) await ready;
if (isInitialApply && prefs.get('disableAll')) {
return {
cfg: {
disableAll: true,
},
};
}
// TODO: enable in FF when it supports sourceURL comment in style elements (also options.html)
const {exposeStyleName} = CHROME && prefs.__values;
const sender = CHROME && this && this.sender || {};
if (sender.frameId === 0) {
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
so we'll use the real URL reported by webNavigation API.
TODO: if FF will do the same, this won't work as is: FF reports onCommitted too late */
url = tabMan.get(sender.tab.id, 'url', 0) || url;
return {disableAll: true};
}
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
so we'll use the real URL reported by webNavigation API */
const {tab, frameId} = this && this.sender || {};
url = tab && tabMan.get(tab.id, 'url', frameId) || url;
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
@ -241,9 +143,9 @@ const styleMan = (() => {
} else if (cache.maybeMatch.size) {
buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean));
}
return Object.assign({cfg: {exposeStyleName, order}},
id ? mapObj(cache.sections, null, [id])
: cache.sections);
return id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
},
/** @returns {Promise<StyleObj>} */
@ -260,12 +162,10 @@ const styleMan = (() => {
const result = [];
const styles = id
? [id2style(id)].filter(Boolean)
: getAllAsArray();
const query = new MatchQuery(url);
: Array.from(dataMap.values(), data2style);
const query = createMatchQuery(url);
for (const style of styles) {
let excluded = false;
let excludedScheme = false;
let included = false;
let sloppy = false;
let sectionMatched = false;
const match = urlMatchStyle(query, style);
@ -273,17 +173,14 @@ const styleMan = (() => {
// if (match === false) {
// continue;
// }
if (match === 'included') {
included = true;
}
if (match === 'excluded') {
excluded = true;
}
if (match === 'excludedScheme') {
excludedScheme = true;
}
for (const section of style.sections) {
const match = urlMatchSection(query, section, true);
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
if (match) {
if (match === 'sloppy') {
sloppy = true;
@ -292,9 +189,8 @@ const styleMan = (() => {
break;
}
}
if (sectionMatched || included) {
result.push(/** @namespace StylesByUrlResult */ {
style, excluded, sloppy, excludedScheme, sectionMatched, included});
if (sectionMatched) {
result.push(/** @namespace StylesByUrlResult */ {style, excluded, sloppy});
}
}
return result;
@ -303,16 +199,18 @@ const styleMan = (() => {
/** @returns {Promise<StyleObj[]>} */
async importMany(items) {
if (ready.then) await ready;
for (const style of items) {
beforeSave(style);
if (style.sourceCode && style.usercssData) {
await usercssMan.buildCode(style);
}
}
const events = await db.styles.putMany(items);
return Promise.all(items.map((item, i) =>
handleSave(item, {reason: 'import'}, events[i])
));
items.forEach(beforeSave);
const events = await db.exec('putMany', items);
return Promise.all(items.map((item, i) => {
afterSave(item, events[i]);
return handleSave(item, 'import');
}));
},
/** @returns {Promise<StyleObj>} */
async import(data) {
if (ready.then) await ready;
return handleSave(await saveStyle(data), 'import');
},
/** @returns {Promise<StyleObj>} */
@ -320,23 +218,46 @@ const styleMan = (() => {
if (ready.then) await ready;
reason = reason || dataMap.has(style.id) ? 'update' : 'install';
style = mergeWithMapped(style);
const url = !style.url && style.updateUrl && (
URLS.extractUsoArchiveInstallUrl(style.updateUrl) ||
URLS.extractGreasyForkInstallUrl(style.updateUrl)
);
if (url) style.url = style.installationUrl = url;
style.originalDigest = await calcStyleDigest(style);
// FIXME: update updateDate? what about usercss config?
return saveStyle(style, {reason});
return handleSave(await saveStyle(style), reason);
},
save: saveStyle,
async setOrder(value) {
/** @returns {Promise<?StyleObj>} */
async putByUUID(doc) {
if (ready.then) await ready;
return setOrder({value}, {broadcast: true, sync: true});
const id = uuidIndex.get(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
const oldDoc = id && id2style(id);
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
API.sync.put(oldDoc._id, oldDoc._rev);
return;
}
}
if (diff < 0) {
doc.id = await db.exec('put', doc);
uuidIndex.set(doc._id, doc.id);
return handleSave(doc, 'sync');
}
},
/** @returns {Promise<number>} style id */
async toggle(id, enabled) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id), {enabled});
await saveStyle(style, {reason: 'toggle'});
handleSave(await saveStyle(style), 'toggle', false);
return id;
},
@ -350,14 +271,6 @@ const styleMan = (() => {
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<?StyleObj>} */
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
async config(id, prop, value) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id));
const {preview = {}} = dataMap.get(id);
style[prop] = preview[prop] = value;
return saveStyle(style, {reason: 'config'});
},
};
//#endregion
@ -370,23 +283,12 @@ const styleMan = (() => {
/** @returns {?StyleObj} */
function id2style(id) {
return (dataMap.get(Number(id)) || {}).style;
return (dataMap.get(id) || {}).style;
}
/** @returns {?StyleObj} */
function uuid2style(uuid) {
return id2style(uuidIndex.get(uuid));
}
function calcRemoteId({md5Url, updateUrl, usercssData: ucd} = {}) {
let id;
id = (id = /\d+/.test(md5Url) || URLS.extractUsoArchiveId(updateUrl)) && `uso-${id}`
|| (id = URLS.extractUSwId(updateUrl)) && `usw-${id}`
|| '';
return id && [
id,
ucd && !isEmptyObj(ucd.vars),
];
function data2style(data) {
return data && data.style;
}
/** @returns {StyleObj} */
@ -407,7 +309,6 @@ const styleMan = (() => {
style,
appliesTo: new Set(),
});
uuidIndex.set(style._id, style.id);
}
/** @returns {StyleObj} */
@ -417,12 +318,10 @@ const styleMan = (() => {
style);
}
function handleDraft(port) {
const id = port.name.split(':').pop();
port.onDisconnect.addListener(() => API.drafts.delete(Number(id) || id));
}
function handleLivePreview(port) {
if (port.name !== 'livePreview') {
return;
}
let id;
port.onMessage.addListener(style => {
if (!id) id = style.id;
@ -450,7 +349,7 @@ const styleMan = (() => {
throw new Error('The rule already exists');
}
style[type] = list.concat([rule]);
return saveStyle(style, {reason: 'config'});
return handleSave(await saveStyle(style), 'styleSettings');
}
async function removeIncludeExclude(type, id, rule) {
@ -461,10 +360,10 @@ const styleMan = (() => {
return;
}
style[type] = list.filter(r => r !== rule);
return saveStyle(style, {reason: 'config'});
return handleSave(await saveStyle(style), 'styleSettings');
}
function broadcastStyleUpdated(style, reason, method = 'styleUpdated') {
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
const {id} = style;
const data = id2data(id);
const excluded = new Set();
@ -474,10 +373,10 @@ const styleMan = (() => {
cache.maybeMatch.add(id);
continue;
}
const code = getAppliedCode(new MatchQuery(url), style);
const code = getAppliedCode(createMatchQuery(url), style);
if (code) {
updated.add(url);
buildCacheEntry(cache, style, code);
cache.sections[id] = {id, code};
} else {
excluded.add(url);
delete cache.sections[id];
@ -487,6 +386,7 @@ const styleMan = (() => {
return msg.broadcast({
method,
reason,
codeIsUpdated,
style: {
id,
md5Url: style.md5Url,
@ -508,39 +408,39 @@ const styleMan = (() => {
style._id = uuidv4();
}
style._rev = Date.now();
fixKnownProblems(style);
fixUsoMd5Issue(style);
}
async function saveStyle(style, handlingOptions) {
function afterSave(style, newId) {
if (style.id == null) {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
API.sync.put(style._id, style._rev);
}
async function saveStyle(style) {
beforeSave(style);
const newId = await db.styles.put(style);
return handleSave(style, handlingOptions, newId);
const newId = await db.exec('put', style);
afterSave(style, newId);
return style;
}
function handleSave(style, {reason, broadcast = true}, id = style.id) {
if (style.id == null) style.id = id;
const data = id2data(id);
function handleSave(style, reason, codeIsUpdated) {
const data = id2data(style.id);
const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) {
storeInMap(style);
} else {
data.style = style;
}
if (reason !== 'sync') {
API.sync.putDoc(style);
}
if (broadcast) broadcastStyleUpdated(style, reason, method);
broadcastStyleUpdated(style, reason, method, codeIsUpdated);
return style;
}
// get styles matching a URL, including sloppy regexps and excluded items.
function getAppliedCode(query, data) {
const result = urlMatchStyle(query, data);
if (result === 'included') {
// return all sections
return data.sections.map(s => s.code);
}
if (result !== true) {
if (urlMatchStyle(query, data) !== true) {
return;
}
const code = [];
@ -553,19 +453,23 @@ const styleMan = (() => {
}
async function init() {
const orderPromise = API.prefsDb.get(orderWrap.id);
const styles = await db.styles.getAll() || [];
const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean));
const styles = await db.exec('getAll') || [];
const updated = styles.filter(style =>
addMissingProps(style) +
addCustomName(style));
if (updated.length) {
await db.styles.putMany(updated);
await db.exec('putMany', updated);
}
for (const style of styles) {
fixUsoMd5Issue(style);
storeInMap(style);
uuidIndex.set(style._id, style.id);
}
setOrder(await orderPromise, {store: false});
styles.forEach(storeInMap);
ready = true;
bgReady._resolveStyles();
}
function fixKnownProblems(style, initIndex, initArray) {
function addMissingProps(style) {
let res = 0;
for (const key in MISSING_PROPS) {
if (!style[key]) {
@ -573,53 +477,22 @@ const styleMan = (() => {
res = 1;
}
}
/* Upgrade the old way of customizing local names */
return res;
}
/** Upgrades the old way of customizing local names */
function addCustomName(style) {
let res = 0;
const {originalName} = style;
if (originalName) {
res = 1;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
res = 1;
}
/* wrong homepage url in 1.5.20-1.5.21 due to commit 1e5f118d */
for (const key of ['url', 'installationUrl']) {
const url = style[key];
const fixedUrl = url && url.replace(/([^:]\/)\//, '$1');
if (fixedUrl !== url) {
res = 1;
style[key] = fixedUrl;
}
}
let url;
/* USO bug, duplicate "update" subdomain, see #523 */
if ((url = style.md5Url) && url.includes('update.update.userstyles')) {
res = style.md5Url = url.replace('update.update.userstyles', 'update.userstyles');
}
/* Default homepage URL for external styles installed from a known distro */
if (
(!style.url || !style.installationUrl) &&
(url = style.updateUrl) &&
(url = URLS.extractGreasyForkInstallUrl(url) ||
URLS.extractUsoArchiveInstallUrl(url) ||
URLS.extractUSwInstallUrl(url)
)
) {
if (!style.url) res = style.url = url;
if (!style.installationUrl) res = style.installationUrl = url;
}
/* @import must precede `vars` that we add at beginning */
if (
initArray &&
!isEmptyObj((style.usercssData || {}).vars) &&
style.sections.some(({code}) =>
code.startsWith(':root {\n --') &&
/@import\s/i.test(code))
) {
return usercssMan.buildCode(style);
}
return res && style;
return res;
}
function urlMatchStyle(query, style) {
@ -632,68 +505,43 @@ const styleMan = (() => {
if (!style.enabled) {
return 'disabled';
}
if (!colorScheme.shouldIncludeStyle(style)) {
return 'excludedScheme';
}
if (
style.inclusions &&
style.inclusions.some(r => compileExclusion(r).test(query.urlWithoutParams))
) {
return 'included';
}
return true;
}
function urlMatchSection(query, section, skipEmptyGlobal) {
let dd, ddL, pp, ppL, rr, rrL, uu, uuL;
function urlMatchSection(query, section) {
if (
(dd = section.domains) && (ddL = dd.length) && dd.some(urlMatchDomain, query) ||
(pp = section.urlPrefixes) && (ppL = pp.length) && pp.some(urlMatchPrefix, query) ||
/* Per the specification the fragment portion is ignored in @-moz-document:
https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
but the spec is outdated and doesn't account for SPA sites,
so we only respect it for `url()` function */
(uu = section.urls) && (uuL = uu.length) && (
uu.includes(query.url) ||
uu.includes(query.urlWithoutHash)
) ||
(rr = section.regexps) && (rrL = rr.length) && rr.some(urlMatchRegexp, query)
section.domains &&
section.domains.some(d => d === query.domain || query.domain.endsWith(`.${d}`))
) {
return true;
}
if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) {
return true;
}
// as per spec the fragment portion is ignored in @-moz-document:
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
// but the spec is outdated and doesn't account for SPA sites
// so we only respect it for `url()` function
if (section.urls && (
section.urls.includes(query.url) ||
section.urls.includes(query.urlWithoutHash)
)) {
return true;
}
if (section.regexps && section.regexps.some(r => compileRe(r).test(query.url))) {
return true;
}
/*
According to CSS4 @document specification the entire URL must match.
Stylish-for-Chrome implemented it incorrectly since the very beginning.
We'll detect styles that abuse the bug by finding the sections that
would have been applied by Stylish but not by us as we follow the spec.
*/
if (rrL && rr.some(urlMatchRegexpSloppy, query)) {
if (section.regexps && section.regexps.some(r => compileSloppyRe(r).test(query.url))) {
return 'sloppy';
}
// TODO: check for invalid regexps?
return !rrL && !ppL && !uuL && !ddL &&
!query.isOwnPage && // We allow only intentionally targeted sections for own pages
(!skipEmptyGlobal || !styleCodeEmpty(section.code));
}
/** @this {MatchQuery} */
function urlMatchDomain(d) {
const _d = this.domain;
return d === _d ||
_d[_d.length - d.length - 1] === '.' && _d.endsWith(d);
}
/** @this {MatchQuery} */
function urlMatchPrefix(p) {
return p && this.url.startsWith(p);
}
/** @this {MatchQuery} */
function urlMatchRegexp(r) {
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
compileRe(r).test(this.url);
}
/** @this {MatchQuery} */
function urlMatchRegexpSloppy(r) {
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
compileSloppyRe(r).test(this.url);
return styleSectionGlobal(section);
}
function createCompiler(compile) {
@ -733,28 +581,82 @@ const styleMan = (() => {
'$';
}
// The md5Url provided by USO includes a duplicate "update" subdomain (see #523),
// This fixes any already installed styles containing this error
function fixUsoMd5Issue(style) {
if (style && style.md5Url && style.md5Url.includes('update.update.userstyles')) {
style.md5Url = style.md5Url.replace('update.update.userstyles', 'update.userstyles');
}
}
function createMatchQuery(url) {
let urlWithoutHash;
let urlWithoutParams;
let domain;
return {
url,
get urlWithoutHash() {
if (!urlWithoutHash) {
urlWithoutHash = url.split('#')[0];
}
return urlWithoutHash;
},
get urlWithoutParams() {
if (!urlWithoutParams) {
const u = createURL(url);
urlWithoutParams = u.origin + u.pathname;
}
return urlWithoutParams;
},
get domain() {
if (!domain) {
const u = createURL(url);
domain = u.hostname;
}
return domain;
},
};
}
function buildCache(cache, url, styleList) {
const query = new MatchQuery(url);
const query = createMatchQuery(url);
for (const {style, appliesTo, preview} of styleList) {
const code = getAppliedCode(query, preview || style);
if (code) {
buildCacheEntry(cache, style, code);
const id = style.id;
cache.sections[id] = {id, code};
appliesTo.add(url);
}
}
}
function buildCacheEntry(cache, style, code = style.code) {
cache.sections[style.id] = {
code,
id: style.id,
name: style.customName || style.name,
};
function createURL(url) {
try {
return new URL(url);
} catch (err) {
return {
hash: '',
host: '',
hostname: '',
href: '',
origin: '',
password: '',
pathname: '',
port: '',
protocol: '',
search: '',
searchParams: new URLSearchParams(),
username: '',
};
}
}
/** @returns {StyleObj[]} */
function getAllAsArray() {
return Array.from(dataMap.values(), v => v.style);
function uuidv4() {
const seeds = crypto.getRandomValues(new Uint16Array(8));
// 00001111-2222-M333-N444-555566667777
seeds[3] = seeds[3] & 0x0FFF | 0x4000; // UUID version 4, M = 4
seeds[4] = seeds[4] & 0x3FFF | 0x8000; // UUID variant 1, N = 8..0xB
return Array.from(seeds, hex4dashed).join('');
}
/** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */
@ -762,30 +664,66 @@ const styleMan = (() => {
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
}
async function setOrder(data, {broadcast, calc = true, store = true, sync} = {}) {
if (!data || !data.value || deepEqual(data.value, orderWrap.value)) {
return;
}
Object.assign(orderWrap, data, sync && {_rev: Date.now()});
if (calc) {
for (const [type, group] of Object.entries(data.value)) {
const dst = order[type] = {};
group.forEach((uuid, i) => {
const id = uuidIndex.get(uuid);
if (id) dst[id] = i;
});
}
}
if (broadcast) {
msg.broadcast({method: 'styleSort', order});
}
if (store) {
await API.prefsDb.put(orderWrap, orderWrap.id);
}
if (sync) {
API.sync.putDoc(orderWrap);
}
}
//#endregion
})();
/** Creates a FIFO limit-size map. */
function createCache({size = 1000, onDeleted} = {}) {
const map = new Map();
const buffer = Array(size);
let index = 0;
let lastIndex = 0;
return {
get(id) {
const item = map.get(id);
return item && item.data;
},
set(id, data) {
if (map.size === size) {
// full
map.delete(buffer[lastIndex].id);
if (onDeleted) {
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
}
lastIndex = (lastIndex + 1) % size;
}
const item = {id, data, index};
map.set(id, item);
buffer[index] = item;
index = (index + 1) % size;
},
delete(id) {
const item = map.get(id);
if (!item) {
return false;
}
map.delete(item.id);
const lastItem = buffer[lastIndex];
lastItem.index = item.index;
buffer[item.index] = lastItem;
lastIndex = (lastIndex + 1) % size;
if (onDeleted) {
onDeleted(item.id, item.data);
}
return true;
},
clear() {
map.clear();
index = lastIndex = 0;
},
has: id => map.has(id),
*entries() {
for (const [id, item] of map) {
yield [id, item.data];
}
},
*values() {
for (const item of map.values()) {
yield item.data;
}
},
get size() {
return map.size;
},
};
}

View File

@ -1,5 +1,5 @@
/* global API */// msg.js
/* global RX_META debounce stringAsRegExp tryRegExp */// toolbox.js
/* global URLS debounce stringAsRegExp tryRegExp */// toolbox.js
/* global addAPI */// common.js
'use strict';
@ -10,12 +10,12 @@
const extractMeta = style =>
style.usercssData
? (style.sourceCode.match(RX_META) || [''])[0]
? (style.sourceCode.match(URLS.rxMETA) || [''])[0]
: null;
const stripMeta = style =>
style.usercssData
? style.sourceCode.replace(RX_META, '')
? style.sourceCode.replace(URLS.rxMETA, '')
: null;
const MODES = Object.assign(Object.create(null), {

View File

@ -56,7 +56,6 @@
return NOP;
}
return API.styles.getSectionsByUrl(url, id).then(sections => {
delete sections.cfg;
const tasks = [];
for (const section of Object.values(sections)) {
const styleId = section.id;

View File

@ -1,5 +1,5 @@
/* global API */// msg.js
/* global CHROME URLS ignoreChromeError */// toolbox.js
/* global CHROME ignoreChromeError */// toolbox.js
/* global prefs */
'use strict';
@ -49,12 +49,6 @@
if (CHROME && !off) {
chrome.webNavigation.onCommitted.addListener(injectData, {url: [{urlPrefix: 'http'}]});
}
if (CHROME) {
chrome.webRequest.onBeforeRequest.addListener(openNamedStyle, {
urls: [URLS.ownOrigin + '*.user.css'],
types: ['main_frame'],
}, ['blocking']);
}
state.csp = csp;
state.off = off;
state.xhr = xhr;
@ -118,8 +112,8 @@
// 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 DOM styles
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');
@ -152,14 +146,6 @@
}
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
function openNamedStyle(req) {
if (!req.url.includes('?')) { // skipping our usercss installer
chrome.tabs.update(req.tabId, {url: 'edit.html?id=' + req.url.split('#')[1]});
return {cancel: true};
}
}
function req2key(req) {
return req.tabId + ':' + req.frameId;
}

View File

@ -1,10 +1,7 @@
/* global API msg */// msg.js
/* global bgReady uuidIndex */// common.js
/* global chromeLocal chromeSync */// storage-util.js
/* global db */
/* global iconMan */
/* global chromeLocal */// storage-util.js
/* global compareRevision */// common.js
/* global prefs */
/* global styleUtil */
/* global tokenMan */
'use strict';
@ -20,7 +17,6 @@ const syncMan = (() => {
disconnecting: 'disconnecting',
});
const STORAGE_KEY = 'sync/state/';
const NO_LOGIN = ['webdav'];
const status = /** @namespace SyncManager.Status */ {
STATES,
state: STATES.disconnected,
@ -30,12 +26,10 @@ const syncMan = (() => {
errorMessage: null,
login: false,
};
const compareRevision = (rev1, rev2) => rev1 - rev2;
let lastError = null;
let ctrl;
let currentDrive;
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = bgReady.styles.then(() => {
let ready = prefs.ready.then(() => {
ready = true;
prefs.subscribe('sync.enabled',
(_, val) => val === 'none'
@ -44,9 +38,9 @@ const syncMan = (() => {
{runNow: true});
});
chrome.alarms.onAlarm.addListener(async ({name}) => {
if (name === 'syncNow') {
await syncMan.syncNow();
chrome.alarms.onAlarm.addListener(info => {
if (info.name === 'syncNow') {
syncMan.syncNow();
}
});
@ -67,67 +61,52 @@ const syncMan = (() => {
return status;
},
async login(name) {
async login(name = prefs.get('sync.enabled')) {
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;
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fails at the first login so we try again
await tokenMan.getToken(name);
}
throw err;
} finally {
emitStatusChange();
}
status.login = true;
emitStatusChange();
},
async putDoc({_id, _rev}) {
async put(...args) {
if (ready.then) await ready;
if (!currentDrive) return;
schedule();
return ctrl.put(_id, _rev);
},
async setDriveOptions(driveName, options) {
const key = `secure/sync/driveOptions/${driveName}`;
await chromeSync.setValue(key, options);
},
async getDriveOptions(driveName) {
const key = `secure/sync/driveOptions/${driveName}`;
return await chromeSync.getValue(key) || {};
return ctrl.put(...args);
},
async start(name, fromPref = false) {
if (ready.then) await ready;
if (!ctrl) await initController();
if (currentDrive) return;
currentDrive = await getDrive(name);
currentDrive = getDrive(name);
ctrl.use(currentDrive);
status.state = STATES.connecting;
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
if (fromPref || NO_LOGIN.includes(currentDrive.name)) {
status.login = true;
} else {
try {
await syncMan.login(name);
} catch (err) {
try {
if (!fromPref) {
await syncMan.login(name).catch(handle401Error);
}
await syncMan.syncNow();
status.errorMessage = null;
} catch (err) {
status.errorMessage = err.message;
// FIXME: should we move this logic to options.js?
if (!fromPref) {
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);
@ -141,7 +120,7 @@ const syncMan = (() => {
status.state = STATES.disconnecting;
emitStatusChange();
try {
await ctrl.uninit();
await ctrl.stop();
await tokenMan.revokeToken(currentDrive.name);
await chromeLocal.remove(STORAGE_KEY + currentDrive.name);
} catch (e) {}
@ -155,21 +134,12 @@ const syncMan = (() => {
async syncNow() {
if (ready.then) await ready;
if (!currentDrive || !status.login) {
console.warn('cannot sync when disconnected');
return;
}
if (!currentDrive) throw new Error('cannot sync when disconnected');
try {
await ctrl.syncNow();
await (ctrl.isInit() ? ctrl.syncNow() : ctrl.start()).catch(handle401Error);
status.errorMessage = null;
lastError = null;
} catch (err) {
err.message = translateErrorMessage(err);
status.errorMessage = err.message;
lastError = err;
if (isGrantError(err)) {
status.login = false;
}
}
emitStatusChange();
},
@ -179,35 +149,19 @@ const syncMan = (() => {
//#region Utils
async function initController() {
await require(['/vendor/db-to-cloud/db-to-cloud']); /* global dbToCloud */
await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
ctrl = dbToCloud.dbToCloud({
onGet: styleUtil.uuid2style,
async onPut(doc) {
const id = uuidIndex.get(doc._id);
const oldCust = uuidIndex.custom[id];
const oldDoc = oldCust || styleUtil.id2style(id);
const diff = oldDoc ? compareRevision(oldDoc._rev, doc._rev) : -1;
if (!diff) return;
if (diff > 0) {
syncMan.putDoc(oldDoc);
} else if (oldCust) {
uuidIndex.custom[id] = doc;
} else {
delete doc.id;
if (id) doc.id = id;
doc.id = await db.styles.put(doc);
await styleUtil.handleSave(doc, {reason: 'sync'});
}
onGet(id) {
return API.styles.getByUUID(id);
},
onDelete(_id, rev) {
const id = uuidIndex.get(_id);
const oldDoc = styleUtil.id2style(id);
return oldDoc &&
compareRevision(oldDoc._rev, rev) <= 0 &&
API.styles.delete(id, 'sync');
onPut(doc) {
return API.styles.putByUUID(doc);
},
onDelete(id, rev) {
return API.styles.deleteByUUID(id, rev);
},
async onFirstSync() {
for (const i of Object.values(uuidIndex.custom).concat(await API.styles.getAll())) {
for (const i of await API.styles.getAll()) {
ctrl.put(i._id, i._rev);
}
},
@ -229,79 +183,43 @@ const syncMan = (() => {
setState(drive, state) {
return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
},
retryMaxAttempts: 10,
retryExp: 1.2,
retryDelay: 6,
});
}
async function handle401Error(err) {
let emit;
if (err.code === 401) {
await tokenMan.revokeToken(currentDrive.name).catch(console.error);
emit = true;
} else if (/User interaction required|Requires user interaction/i.test(err.message)) {
emit = true;
}
if (emit) {
status.login = false;
emitStatusChange();
}
return Promise.reject(err);
}
function emitStatusChange() {
msg.broadcastExtension({method: 'syncStatusUpdate', status});
iconMan.overrideBadge(getErrorBadge());
}
function isNetworkError(err) {
return (
err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message) ||
err.code === 502
);
}
function isGrantError(err) {
if (err.code === 401) return true;
if (err.code === 400 && /invalid_grant/.test(err.message)) return true;
if (err.name === 'TokenError') return true;
return false;
}
function getErrorBadge() {
if (status.state === STATES.connected &&
(!status.login || lastError && !isNetworkError(lastError))) {
return {
text: 'x',
color: '#F00',
title: !status.login ? 'syncErrorRelogin' : `${
chrome.i18n.getMessage('syncError')
}\n---------------------\n${
// splitting to limit each line length
lastError.message.replace(/.{60,}?\s(?=.{30,})/g, '$&\n')
}`,
};
}
}
async function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive' || name === 'webdav') {
const options = await syncMan.getDriveOptions(name);
options.getAccessToken = () => tokenMan.getToken(name);
options.fetch = name === 'webdav' ? fetchWebDAV.bind(options) : fetch;
return dbToCloud.drive[name](options);
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}`);
}
/** @this {Object} DriveOptions */
function fetchWebDAV(url, init = {}) {
init.credentials = 'omit'; // circumventing nextcloud CSRF token error
init.headers = Object.assign({}, init.headers, {
Authorization: `Basic ${btoa(`${this.username || ''}:${this.password || ''}`)}`,
});
return fetch(url, init);
}
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
delayInMinutes: delay, // fractional values are supported
delayInMinutes: delay,
periodInMinutes: SYNC_INTERVAL,
});
}
function translateErrorMessage(err) {
if (err.name === 'LockError') {
return browser.i18n.getMessage('syncErrorLock', new Date(err.expire).toLocaleString([], {timeStyle: 'short'}));
}
return err.message || String(err);
}
//#endregion
})();

View File

@ -53,7 +53,6 @@ const tabMan = (() => {
}
},
/** @returns {IterableIterator<number>} */
list() {
return cache.keys();
},

View File

@ -1,4 +1,4 @@
/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js
/* global FIREFOX */// toolbox.js
/* global chromeLocal */// storage-util.js
'use strict';
@ -32,11 +32,10 @@ const tokenMan = (() => {
},
tokenURL: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
// FIXME: https://github.com/openstyles/stylus/issues/1248
// revoke: token => {
// const params = {token};
// return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
// },
revoke: token => {
const params = {token};
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
},
},
onedrive: {
flow: 'code',
@ -44,42 +43,21 @@ const tokenMan = (() => {
clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
redirect_uri: FIREFOX ?
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
'https://' + location.hostname + '.chromiumapp.org/',
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
},
userstylesworld: {
flow: 'code',
clientId: 'zeDmKhJIfJqULtcrGMsWaxRtWHEimKgS',
clientSecret: 'wqHsvTuThQmXmDiVvOpZxPwSIbyycNFImpAOTxjaIRqDbsXcTOqrymMJKsOMuibFaij' +
'ZZAkVYTDbLkQuYFKqgpMsMlFlgwQOYHvHFbgxQHDTwwdOroYhOwFuekCwXUlk',
authURL: URLS.usw + 'api/oauth/style/link',
tokenURL: URLS.usw + 'api/oauth/token',
redirect_uri: 'https://gusted.xyz/callback_helper/',
},
};
const NETWORK_LATENCY = 30; // seconds
const DEFAULT_REDIRECT_URI = 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/';
let alwaysUseTab = !chrome.windows || (FIREFOX ? false : null);
class TokenError extends Error {
constructor(provider, message) {
super(`[${provider}] ${message}`);
this.name = 'TokenError';
this.provider = provider;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TokenError);
}
}
}
return {
buildKeys(name, hooks) {
const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
buildKeys(name) {
const k = {
TOKEN: `${prefix}token`,
EXPIRE: `${prefix}expire`,
REFRESH: `${prefix}refresh`,
TOKEN: `secure/token/${name}/token`,
EXPIRE: `secure/token/${name}/expire`,
REFRESH: `secure/token/${name}/refresh`,
};
k.LIST = Object.values(k);
return k;
@ -89,26 +67,27 @@ const tokenMan = (() => {
return AUTH[name].clientId;
},
async getToken(name, interactive, hooks) {
const k = tokenMan.buildKeys(name, hooks);
async getToken(name, interactive) {
const k = tokenMan.buildKeys(name);
const obj = await chromeLocal.get(k.LIST);
if (obj[k.TOKEN]) {
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
return obj[k.TOKEN];
}
if (obj[k.REFRESH]) {
return refreshToken(name, k, obj);
try {
return await refreshToken(name, k, obj);
} catch (err) {
if (err.code !== 401) throw err;
}
}
}
if (!interactive) {
throw new TokenError(name, 'Token is missing');
}
return authUser(k, name, interactive, hooks);
return authUser(name, k, interactive);
},
async revokeToken(name, hooks) {
async revokeToken(name) {
const provider = AUTH[name];
const k = tokenMan.buildKeys(name, hooks);
const k = tokenMan.buildKeys(name);
if (provider.revoke) {
try {
const token = await chromeLocal.getValue(k.TOKEN);
@ -123,7 +102,7 @@ const tokenMan = (() => {
async function refreshToken(name, k, obj) {
if (!obj[k.REFRESH]) {
throw new TokenError(name, 'No refresh token');
throw new Error('No refresh token');
}
const provider = AUTH[name];
const body = {
@ -143,15 +122,15 @@ const tokenMan = (() => {
return handleTokenResult(result, k);
}
async function authUser(keys, name, interactive = false, hooks = null) {
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow']);
async function authUser(name, k, interactive = false) {
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min']);
/* global webextLaunchWebAuthFlow */
const provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2);
const query = {
response_type: provider.flow,
client_id: provider.clientId,
redirect_uri: provider.redirect_uri || DEFAULT_REDIRECT_URI,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
state,
};
if (provider.scopes) {
@ -160,28 +139,11 @@ const tokenMan = (() => {
if (provider.authQuery) {
Object.assign(query, provider.authQuery);
}
if (alwaysUseTab == null) {
alwaysUseTab = await detectVivaldiWebRequestBug();
}
if (hooks) hooks.query(query);
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
const width = Math.min(screen.availWidth - 100, 800);
const height = Math.min(screen.availHeight - 100, 800);
const wnd = !alwaysUseTab && await browser.windows.getLastFocused();
const finalUrl = await webextLaunchWebAuthFlow({
url,
alwaysUseTab,
interactive,
redirect_uri: query.redirect_uri,
windowOptions: wnd && Object.assign({
state: 'normal',
width,
height,
}, wnd.state !== 'minimized' && {
// Center the popup to the current window
top: Math.ceil(wnd.top + (wnd.height - width) / 2),
left: Math.ceil(wnd.left + (wnd.width - width) / 2),
}),
});
const params = new URLSearchParams(
provider.flow === 'token' ?
@ -189,7 +151,7 @@ const tokenMan = (() => {
new URL(finalUrl).search.slice(1)
);
if (params.get('state') !== state) {
throw new TokenError(name, `Unexpected state: ${params.get('state')}, expected: ${state}`);
throw new Error(`Unexpected state: ${params.get('state')}, expected: ${state}`);
}
let result;
if (provider.flow === 'token') {
@ -205,14 +167,13 @@ const tokenMan = (() => {
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);
return handleTokenResult(result, k);
}
async function handleTokenResult(result, k) {
@ -243,28 +204,4 @@ const tokenMan = (() => {
err.code = r.status;
throw err;
}
async function detectVivaldiWebRequestBug() {
// Workaround for https://github.com/openstyles/stylus/issues/1182
// Note that modern Vivaldi isn't exposed in `navigator.userAgent` but it adds `extData` to tabs
const anyTab = await getActiveTab() || (await browser.tabs.query({}))[0];
if (anyTab && !(anyTab.extData || anyTab.vivExtData)) {
return false;
}
let bugged = true;
const TEST_URL = chrome.runtime.getURL('manifest.json');
const check = ({url}) => {
bugged = url !== TEST_URL;
};
chrome.webRequest.onBeforeRequest.addListener(check, {urls: [TEST_URL], types: ['main_frame']});
const {tabs: [tab]} = await browser.windows.create({
type: 'popup',
state: 'minimized',
url: TEST_URL,
});
await waitForTabUrl(tab);
chrome.windows.remove(tab.windowId);
chrome.webRequest.onBeforeRequest.removeListener(check);
return bugged;
}
})();

View File

@ -1,9 +1,7 @@
/* global API */// msg.js
/* global RX_META URLS debounce deepMerge download ignoreChromeError */// toolbox.js
/* global calcStyleDigest styleSectionsEqual */ // sections-util.js
/* global 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';
@ -23,22 +21,13 @@ const updateMan = (() => {
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
const USO_STYLES_API = `${URLS.uso}api/v1/styles/`;
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
const RX_DATE2VER = new RegExp([
/^(\d{4})/,
/(0[1-9]|1(?:0|[12](?=\d\d))?|[2-9])/, // in ambiguous cases like yyyy123 the month will be 1
/(0[1-9]|[1-2][0-9]?|3[0-1]?|[4-9])/,
/\.([01][0-9]?|2[0-3]?|[3-9])/,
/\.([0-5][0-9]?|[6-9])$/,
].map(rx => rx.source).join(''));
const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3;
const RETRY_ERRORS = [
503, // service unavailable
429, // too many requests
];
let usoReferers = 0;
let lastUpdateTime;
let checkingAll = false;
let logQueue = [];
@ -65,7 +54,7 @@ const updateMan = (() => {
checkingAll = true;
const port = observe && chrome.runtime.connect({name: 'updater'});
const styles = (await API.styles.getAll())
.filter(style => style.updateUrl && style.updatable !== false);
.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
@ -80,17 +69,17 @@ const updateMan = (() => {
/**
* @param {{
id?: number,
style?: StyleObj,
port?: chrome.runtime.Port,
save?: boolean,
ignoreDigest?: boolean,
id?: number
style?: StyleObj
port?: chrome.runtime.Port
save?: boolean = true
ignoreDigest?: boolean
}} opts
* @returns {{
style: StyleObj,
updated?: boolean,
error?: any,
STATES: UpdaterStates,
style: StyleObj
updated?: boolean
error?: any
STATES: UpdaterStates
}}
Original style digests are calculated in these cases:
@ -107,21 +96,19 @@ const updateMan = (() => {
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
async function checkStyle(opts) {
let {id} = opts;
const {
id,
style = await API.styles.get(id),
ignoreDigest,
port,
save,
} = opts;
if (!id) id = style.id;
const {md5Url} = style;
let {usercssData: ucd, updateUrl} = style;
const ucd = style.usercssData;
let res, state;
try {
await checkIfEdited();
res = {
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
updated: true,
};
state = STATES.UPDATED;
@ -130,9 +117,9 @@ const updateMan = (() => {
err && err.message ||
err;
res = {error, style, STATES};
state = `${STATES.SKIPPED} (${Array.isArray(err) ? err[0].message : error})`;
state = `${STATES.SKIPPED} (${error})`;
}
log(`${state} #${id} ${style.customName || style.name}`);
log(`${state} #${style.id} ${style.customName || style.name}`);
if (port) port.postMessage(res);
return res;
@ -145,81 +132,53 @@ const updateMan = (() => {
}
async function updateUSO() {
const md5 = await tryDownload(md5Url);
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);
}
let varsUrl = '';
if (!ucd) {
ucd = {};
varsUrl = updateUrl;
updateUrl = style.updateUrl = `${USO_STYLES_API}${md5Url.match(/\/(\d+)/)[1]}`;
}
usoSpooferStart();
let json;
try {
json = await tryDownload(style.updateUrl, {responseType: 'json'});
json = await updateUsercss(json.css) ||
(await API.uso.toUsercss(json)).style;
if (varsUrl) await API.uso.useVarsUrl(json, varsUrl);
} finally {
usoSpooferStop();
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 updateUsercss(css) {
let oldVer = ucd.version;
let {etag: oldEtag, updateUrl} = style;
const m2 = (css || URLS.extractUsoArchiveId(updateUrl)) &&
await getUsoEmbeddedMeta(css);
if (m2 && m2.updateUrl) {
updateUrl = m2.updateUrl;
oldVer = m2.usercssData.version || '0';
oldEtag = '';
} else if (css) {
return;
}
if (oldEtag && oldEtag === await downloadEtag()) {
return Promise.reject(STATES.SAME_CODE);
}
async function updateUsercss() {
// 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;
const url = style.updateUrl;
const metaUrl = URLS.extractGreasyForkInstallUrl(url) &&
url.replace(/\.user\.css$/, '.meta.css');
const text = await tryDownload(metaUrl || url);
const json = await API.usercss.buildMeta({sourceCode: text});
await require(['/vendor/semver-bundle/semver']); /* global semverCompare */
const delta = semverCompare(json.usercssData.version, ucd.version);
if (!delta && !ignoreDigest) {
// re-install is invalid in a soft upgrade
err = response === style.sourceCode
? STATES.SAME_CODE
: !URLS.isLocalhost(updateUrl) && STATES.SAME_VERSION;
const sameCode = !metaUrl && text === style.sourceCode;
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
}
if (delta < 0) {
// downgrade is always invalid
err = STATES.ERROR_VERSION;
return Promise.reject(STATES.ERROR_VERSION);
}
if (err && etag && !style.etag) {
// first check of ETAG, gonna write it directly to DB as it's too trivial to sync or announce
style.etag = etag;
await db.styles.put(style);
if (metaUrl) {
json.sourceCode = await tryDownload(url);
}
return err
? Promise.reject(err)
: API.usercss.buildCode(json);
return API.usercss.buildCode(json);
}
async function maybeSave(json) {
json.id = id;
json.id = style.id;
json.updateDate = Date.now();
// 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;
@ -236,7 +195,6 @@ const updateMan = (() => {
let {retryDelay = 1000} = opts;
while (true) {
try {
params = deepMerge(params || {}, {headers: {'Cache-Control': 'no-cache'}});
return await download(url, params);
} catch (code) {
if (!RETRY_ERRORS.includes(code) ||
@ -248,27 +206,6 @@ const updateMan = (() => {
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
async function downloadEtag() {
const opts = Object.assign({method: 'head'}, RH_ETAG);
const req = await tryDownload(style.updateUrl, opts);
return req.headers.etag;
}
function getDateFromVer(style) {
const m = RX_DATE2VER.exec((style.usercssData || {}).version);
if (m) {
m[2]--; // month is 0-based in `Date` constructor
return new Date(...m.slice(1)).getTime();
}
}
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
function getUsoEmbeddedMeta(code = style.sourceCode) {
const isRaw = arguments[0];
const m = code.includes('@updateURL') && (isRaw ? code : code.replace(RX_META, '')).match(RX_META);
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
}
}
function schedule() {
@ -317,32 +254,4 @@ const updateMan = (() => {
logLastWriteTime = Date.now();
logQueue = [];
}
function usoSpooferStart() {
if (++usoReferers === 1) {
chrome.webRequest.onBeforeSendHeaders.addListener(
usoSpoofer,
{types: ['xmlhttprequest'], urls: [USO_STYLES_API + '*']},
['blocking', 'requestHeaders', chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS]
.filter(Boolean));
}
}
function usoSpooferStop() {
if (--usoReferers <= 0) {
usoReferers = 0;
chrome.webRequest.onBeforeSendHeaders.removeListener(usoSpoofer);
}
}
/** @param {chrome.webRequest.WebResponseHeadersDetails | browser.webRequest._OnBeforeSendHeadersDetails} info */
function usoSpoofer(info) {
if (info.tabId < 0 && URLS.ownOrigin.startsWith(info.initiator || info.originUrl || '')) {
const {requestHeaders: hh} = info;
const i = (hh.findIndex(h => /^referer$/i.test(h.name)) + 1 || hh.push({})) - 1;
hh[i].name = 'referer';
hh[i].value = URLS.uso;
return {requestHeaders: hh};
}
}
})();

View File

@ -1,4 +1,4 @@
/* global RX_META URLS download openURL */// toolbox.js
/* global URLS download openURL */// toolbox.js
/* global addAPI bgReady */// common.js
/* global tabMan */// msg.js
'use strict';
@ -35,9 +35,9 @@ bgReady.all.then(() => {
chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
urls: [
URLS.usw + 'api/style/*.user.css',
...URLS.usoArchiveRaw.map(s => s + 'usercss/*.user.css'),
...['greasy', 'sleazy'].map(s => `*://${s}fork.org/scripts/*/code/*.user.css`),
URLS.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css',
'*://sleazyfork.org/scripts/*/code/*.user.css',
...[].concat(
...Object.entries(maybeDistro)
.map(([host, {glob}]) => makeUsercssGlobs(host, glob))),
@ -74,10 +74,6 @@ bgReady.all.then(() => {
) && download(url);
}
function makeInstallerUrl(url) {
return `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
}
function makeUsercssGlobs(host, path) {
return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
}
@ -86,11 +82,11 @@ bgReady.all.then(() => {
if (url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!oldUrl.startsWith(makeInstallerUrl(url))) {
!oldUrl.startsWith(URLS.installUsercss)) {
const inTab = url.startsWith('file:') && !chrome.app;
const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
if (!/^\s*</.test(code) && RX_META.test(code)) {
await openInstallerPage(tabId, url, {code, inTab});
if (!/^\s*</.test(code) && URLS.rxMETA.test(code)) {
openInstallerPage(tabId, url, {code, inTab});
}
}
}
@ -103,33 +99,25 @@ bgReady.all.then(() => {
openInstallerPage(tabId, url, {});
// Silently suppress navigation.
// Don't redirect to the install URL as it'll flash the text!
return {cancel: true};
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
}
}
async function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = makeInstallerUrl(url);
function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
const tab = await browser.tabs.get(tabId);
return openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
});
}
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
try {
await browser.tabs.update(tabId, {url: newUrl});
} catch (err) {
// FIXME: remove this when kiwi supports tabs.update
// https://github.com/openstyles/stylus/issues/1367
if (/Tabs cannot be edited right now/i.test(err.message)) {
return browser.tabs.create({url: newUrl});
}
throw err;
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
}
}

View File

@ -1,5 +1,5 @@
/* global API */// msg.js
/* global RX_META deepCopy download */// toolbox.js
/* global URLS deepCopy download */// toolbox.js
'use strict';
const usercssMan = {
@ -12,12 +12,10 @@ const usercssMan = {
name: null,
}),
/** `src` is a style or vars */
async assignVars(style, src) {
async assignVars(style, oldStyle) {
const meta = style.usercssData;
const meta2 = src.usercssData;
const {vars} = meta;
const oldVars = meta2 ? meta2.vars : src;
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)) {
@ -42,31 +40,27 @@ const usercssMan = {
const style = await usercssMan.buildMeta({sourceCode});
const dup = (checkDup || assignVars) &&
await usercssMan.find(styleId ? {id: styleId} : style);
let log;
if (!metaOnly) {
if (vars || assignVars) {
await usercssMan.assignVars(style, vars || dup);
await usercssMan.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};
return {style, dup};
},
async buildCode(style) {
const {sourceCode: code, usercssData: {vars, preprocessor}} = style;
const match = code.match(RX_META);
const match = code.match(URLS.rxMETA);
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 {sections, errors} = 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;
},
@ -80,7 +74,7 @@ const usercssMan = {
enabled: true,
sections: [],
}, style);
const match = code.match(RX_META);
const match = code.match(URLS.rxMETA);
if (!match) {
return Promise.reject(new Error('Could not find metadata.'));
}
@ -117,11 +111,7 @@ const usercssMan = {
},
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),
};
return API.styles.editSave(await usercssMan.parse(style));
},
async find(styleOrData) {
@ -139,18 +129,17 @@ const usercssMan = {
}
},
async install(style, opts) {
return API.styles.install(await usercssMan.parse(style, opts));
async install(style) {
return API.styles.install(await usercssMan.parse(style));
},
async parse(style, {dup, vars} = {}) {
async parse(style) {
style = await usercssMan.buildMeta(style);
// preserve style.vars during update
if (dup || (dup = await usercssMan.find(style))) {
const dup = await usercssMan.find(style);
if (dup) {
style.id = dup.id;
}
if (vars || (vars = dup)) {
await usercssMan.assignVars(style, vars);
await usercssMan.assignVars(style, dup);
}
return usercssMan.buildCode(style);
},

View File

@ -1,158 +0,0 @@
/* global URLS stringAsRegExp */// toolbox.js
/* global usercssMan */
'use strict';
const usoApi = {};
(() => {
const pingers = {};
usoApi.pingback = (usoId, delay) => {
clearTimeout(pingers[usoId]);
delete pingers[usoId];
if (delay > 0) {
return new Promise(resolve => (pingers[usoId] = setTimeout(ping, delay, usoId, resolve)));
} else if (delay !== false) {
return ping(usoId);
}
};
/**
* Replicating USO-Archive format
* https://github.com/33kk/uso-archive/blob/flomaster/lib/uso.js
* https://github.com/33kk/uso-archive/blob/flomaster/lib/converters.js
*/
usoApi.toUsercss = async (data, {metaOnly = true, varsUrl} = {}) => {
const badKeys = {};
const newKeys = [];
const descr = JSON.stringify(data.description.trim());
const vars = (data.style_settings || []).map(makeVar, {badKeys, newKeys}).join('');
const sourceCode = `\
/* ==UserStyle==
@name ${data.name}
@namespace USO Archive
@version ${data.updated.replace(/-/g, '').replace(/[T:]/g, '.').slice(0, 14)}
@description ${/^"['`]|\\/.test(descr) ? descr : descr.slice(1, -1)}
@author ${(data.user || {}).name || '?'}
@license ${makeLicense(data.license)}${vars ? '\n@preprocessor uso' + vars : ''}`
.replace(/\*\//g, '*\\/') +
`==/UserStyle== */\n${newKeys[0] ? useNewKeys(data.css, badKeys) : data.css}`;
const {style} = await usercssMan.build({sourceCode, metaOnly});
usoApi.useVarsUrl(style, varsUrl);
return {style, badKeys, newKeys};
};
usoApi.useVarsUrl = (style, url) => {
if (!/\?ik-/.test(url)) {
return;
}
const cfg = {badKeys: {}, newKeys: []};
const {vars} = style.usercssData;
if (!vars) {
return;
}
for (let [key, val] of new URLSearchParams(url.split('?')[1])) {
if (!key.startsWith('ik-')) continue;
key = makeKey(key.slice(3), cfg);
const v = vars[key];
if (!v) continue;
if (v.options) {
let sel = val.startsWith('ik-') && optByName(v, makeKey(val.slice(3), cfg));
if (!sel) {
key += '-custom';
sel = optByName(v, key + '-dropdown');
if (sel) vars[key].value = val;
}
if (sel) v.value = sel.name;
} else {
v.value = val;
}
}
return true;
};
async function ping(id, resolve) {
await fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`);
if (resolve) resolve(true);
return true;
}
function makeKey(key, {badKeys, newKeys}) {
let res = badKeys[key];
if (!res) {
res = key.replace(/[^-\w]/g, '-');
res += newKeys.includes(res) ? '-' : '';
if (key !== res) {
badKeys[key] = res;
newKeys.push(res);
}
}
return res;
}
function makeLicense(s) {
return !s ? 'NO-REDISTRIBUTION' :
s === 'publicdomain' ? 'CC0-1.0' :
s.startsWith('ccby') ? `${s.toUpperCase().match(/(..)/g).join('-')}-4.0` :
s;
}
function makeVar({
label,
setting_type: type,
install_key: ik,
style_setting_options: opts,
}) {
const cfg = this;
let value, suffix;
ik = makeKey(ik, cfg);
label = JSON.stringify(label);
switch (type) {
case 'color':
value = opts[0].value;
break;
case 'text':
value = JSON.stringify(opts[0].value);
break;
case 'image': {
const ikCust = `${ik}-custom`;
opts.push({
label: 'Custom',
install_key: `${ikCust}-dropdown`,
value: `/*[[${ikCust}]]*/`,
});
suffix = `\n@advanced text ${ikCust} ${label.slice(0, -1)} (Custom)" "https://foo.com/123.jpg"`;
type = 'dropdown';
} // fallthrough
case 'dropdown':
value = '';
for (const o of opts) {
const def = o.default ? '*' : '';
const val = o.value;
const s = ` ${makeKey(o.install_key, cfg)} ${JSON.stringify(o.label + def)} <<<EOT${
val.includes('\n') ? '\n' : ' '}${val} EOT;\n`;
value = def ? s + value : value + s;
}
value = `{\n${value}}`;
break;
default:
value = '"ERROR: unknown type"';
}
return `\n@advanced ${type} ${ik} ${label} ${value}${suffix || ''}`;
}
function optByName(v, name) {
return v.options.find(o => o.name === name);
}
function useNewKeys(css, badKeys) {
const rxsKeys = stringAsRegExp(Object.keys(badKeys).join('\n'), '', true).replace(/\n/g, '|');
const rxUsoVars = new RegExp(`(/\\*\\[\\[)(${rxsKeys})(?=]]\\*/)`, 'g');
return css.replace(rxUsoVars, (s, a, key) => a + badKeys[key]);
}
})();

View File

@ -1,120 +0,0 @@
/* global API msg */// msg.js
/* global URLS */ // toolbox.js
/* global tokenMan */
'use strict';
const uswApi = (() => {
//#region Internals
class TokenHooks {
constructor(id) {
this.id = id;
}
keyName(name) {
return `${name}/${this.id}`;
}
query(query) {
return Object.assign(query, {vendor_data: this.id});
}
}
function fakeUsercssHeader(style) {
const {name, _usw: u = {}} = style;
const meta = Object.entries({
'@name': u.name || name || '?',
'@version': // Same as USO-archive version: YYYYMMDD.hh.mm
new Date().toISOString().replace(/^(\d+)-(\d+)-(\d+)T(\d+):(\d+).+/, '$1$2$3.$4.$5'),
'@namespace': u.namespace !== '?' && u.namespace ||
u.username && `userstyles.world/user/${u.username}` ||
'?',
'@description': u.description,
'@author': u.username,
'@license': u.license,
});
const maxKeyLen = meta.reduce((res, [k]) => Math.max(res, k.length), 0);
return [
'/* ==UserStyle==',
...meta.map(([k, v]) => v && `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v}`).filter(Boolean),
'==/UserStyle== */',
].join('\n') + '\n\n';
}
async function linkStyle(style, sourceCode) {
const {id} = style;
const metadata = await API.worker.parseUsercssMeta(sourceCode).catch(console.warn) || {};
const uswData = Object.assign({}, style, {metadata, sourceCode});
API.data.set('usw' + id, uswData);
const token = await tokenMan.getToken('userstylesworld', true, new TokenHooks(id));
const info = await uswFetch('style', token);
const data = style._usw = Object.assign({token}, info);
style.url = style.url || data.homepage || `${URLS.usw}style/${data.id}`;
await uswSave(style);
return data;
}
async function uswFetch(path, token, opts) {
opts = Object.assign({credentials: 'omit'}, opts);
opts.headers = Object.assign({Authorization: `Bearer ${token}`}, opts.headers);
return (await (await fetch(`${URLS.usw}api/${path}`, opts)).json()).data;
}
/** Uses a custom method when broadcasting and avoids needlessly sending the entire style */
async function uswSave(style) {
const {id, _usw} = style;
await API.styles.save(style, {broadcast: false});
msg.broadcastExtension({method: 'uswData', style: {id, _usw}});
}
//#endregion
//#region Exports
return {
/**
* @param {number} id
* @param {string} sourceCode
* @return {Promise<string>}
*/
async publish(id, sourceCode) {
const style = await API.styles.get(id);
const code = style.usercssData ? sourceCode
: fakeUsercssHeader(style) + sourceCode;
const data = (style._usw || {}).token
? style._usw
: await linkStyle(style, code);
return uswFetch(`style/${data.id}`, data.token, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code}),
});
},
/**
* @param {number} id
* @return {Promise<void>}
*/
async revoke(id) {
await tokenMan.revokeToken('userstylesworld', new TokenHooks(id));
const style = await API.styles.get(id);
if (style) {
style._usw = {};
await uswSave(style);
}
},
};
//#endregion
})();
/* Doing this outside so we don't break IDE's recognition of the exported methods in IIFE */
for (const [k, fn] of Object.entries(uswApi)) {
uswApi[k] = async (id, ...args) => {
API.data.set('usw' + id, true);
try {
/* Awaiting inside `try` so that `finally` runs when done */
return await fn(id, ...args);
} finally {
API.data.del('usw' + id);
}
};
}

View File

@ -5,30 +5,18 @@
(() => {
if (window.INJECTED === 1) return;
window.INJECTED = 1;
/** true -> when the page styles are received,
* false -> when disableAll mode is on at start, the styles won't be sent
* so while disableAll lasts we can ignore messages about style updates because
* the tab will explicitly ask for all styles in bulk when disableAll mode ends */
let hasStyles = false;
let isDisabled = false;
let isTab = !chrome.tabs || location.pathname !== '/popup.html';
const order = {main: [], prio: []};
const calcOrder = ({id}) =>
(order.prio[id] || 0) * 1e6 ||
order.main[id] ||
id + .5e6; // no order = at the end of `main`
const isFrame = window !== parent;
const isFrameAboutBlank = isFrame && location.href === 'about:blank';
const isUnstylable = !chrome.app && document instanceof XMLDocument;
const styleInjector = StyleInjector({
compare: (a, b) => calcOrder(a) - calcOrder(b),
compare: (a, b) => a.id - b.id,
onUpdate: onInjectorUpdate,
});
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
location.href;
// dynamic about: and javascript: iframes don't have a URL yet so we'll use their parent
const matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href) || location.href;
// save it now because chrome.runtime will be unavailable in the orphaned script
const orphanEventId = chrome.runtime.id;
@ -41,26 +29,8 @@
let lazyBadge = isFrame;
let parentDomain;
/* about:blank iframes are often used by sites for file upload or background tasks
* and they may break if unexpected DOM stuff is present at `load` event
* so we'll add the styles only if the iframe becomes visible */
const xoEventId = `${Math.random()}`;
/** @type IntersectionObserver */
let xo;
window[Symbol.for('xo')] = (el, cb) => {
if (!xo) xo = new IntersectionObserver(onIntersect, {rootMargin: '100%'});
el.addEventListener(xoEventId, cb, {once: true});
xo.observe(el);
};
// FIXME: move this to background page when following bugs are fixed:
// https://bugzil.la/1587723, https://crbug.com/968651
const mqDark = matchMedia('(prefers-color-scheme: dark)');
mqDark.onchange = e => API.colorScheme.updateSystemPreferDark(e.matches);
mqDark.onchange(mqDark);
// Declare all vars before init() or it'll throw due to "temporal dead zone" of const/let
init();
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) {
@ -71,11 +41,6 @@
}
msg.onTab(applyOnMessage);
window.addEventListener('pageshow', e => {
if (e.isTrusted && e.persisted) { // bfcache
updateCount();
}
});
if (!chrome.tabs) {
window.dispatchEvent(new CustomEvent(orphanEventId));
@ -100,18 +65,13 @@
} else {
const SYM_ID = 'styles';
const SYM = Symbol.for(SYM_ID);
const parentStyles = isFrameAboutBlank &&
tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
const styles =
window[SYM] ||
parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
!isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
(isFrameAboutBlank
? tryCatch(() => parent[parent.Symbol.for(SYM_ID)])
: chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr)) ||
await API.styles.getSectionsByUrl(matchUrl, null, true);
if (styles.cfg) {
isDisabled = styles.cfg.disableAll;
Object.assign(order, styles.cfg.order);
}
hasStyles = !isDisabled;
hasStyles = !styles.disableAll;
if (hasStyles) {
window[SYM] = styles;
await styleInjector.apply(styles);
@ -119,7 +79,6 @@
delete window[SYM];
prefs.subscribe('disableAll', updateDisableAll);
}
styleInjector.toggle(hasStyles);
}
}
@ -157,7 +116,6 @@
break;
case 'styleUpdated':
if (!hasStyles && isDisabled) break;
if (style.enabled) {
API.styles.getSectionsByUrl(matchUrl, style.id).then(sections =>
sections[style.id]
@ -169,27 +127,26 @@
break;
case 'styleAdded':
if (!hasStyles && isDisabled) break;
if (style.enabled) {
API.styles.getSectionsByUrl(matchUrl, style.id)
.then(styleInjector.apply);
}
break;
case 'styleSort':
Object.assign(order, request.order);
styleInjector.sort();
break;
case 'urlChanged':
if (!hasStyles && isDisabled || matchUrl === request.url) break;
matchUrl = request.url;
API.styles.getSectionsByUrl(matchUrl).then(sections => {
hasStyles = true;
styleInjector.replace(sections);
});
break;
case 'backgroundReady':
ready.catch(err =>
msg.isIgnorableError(err)
? init()
: console.error(err));
break;
case 'updateCount':
updateCount();
break;
@ -197,7 +154,6 @@
}
function updateDisableAll(key, disableAll) {
isDisabled = disableAll;
if (isUnstylable) {
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
} else if (!hasStyles && !disableAll) {
@ -238,20 +194,6 @@
).catch(msg.ignoreError);
}
function onFrameElementInView(cb) {
parent[parent.Symbol.for('xo')](frameElement, cb);
}
/** @param {IntersectionObserverEntry[]} entries */
function onIntersect(entries) {
for (const e of entries) {
if (e.isIntersecting) {
xo.unobserve(e.target);
e.target.dispatchEvent(new Event(xoEventId));
}
}
}
function tryCatch(func, ...args) {
try {
return func(...args);
@ -259,13 +201,12 @@
}
function orphanCheck() {
if (chrome.runtime.id) return;
if (tryCatch(() => chrome.i18n.getUILanguage())) return;
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
window.removeEventListener(orphanEventId, orphanCheck, true);
mqDark.onchange = null;
isOrphaned = true;
setTimeout(styleInjector.clear, 1000); // avoiding FOUC
styleInjector.clear();
tryCatch(msg.off, applyOnMessage);
}
})();

View File

@ -0,0 +1,174 @@
/* global API */// msg.js
'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.usercss.find({
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 = event => {
// 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(event);
}
};
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.usercss.install({
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();
})();

View File

@ -1,324 +1,378 @@
/* global API */// msg.js
/* global API msg */// msg.js
'use strict';
// eslint-disable-next-line no-unused-expressions
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
if (window.INJECTED_USO === 1) return;
window.INJECTED_USO = 1;
const usoId = RegExp.$1;
const USO = 'https://userstyles.org';
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
const CLICK = [
['#install_stylish_style_button', onInstall],
['#update_stylish_style_button', onInstall],
['.customize_style_button', onCustomize],
['.uninstall_stylish_style_button', onUninstall],
];
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
const styleId = RegExp.$1;
const pageEventId = `${performance.now()}${Math.random()}`;
const contentEventId = pageEventId + ':';
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
const $ = (sel, base = document) => base.querySelector(sel);
const toggleListener = (isOn, ...args) => (isOn ? addEventListener : removeEventListener)(...args);
const togglePageListener = isOn => toggleListener(isOn, contentEventId, onPageEvent, true);
const mo = new MutationObserver(onMutation);
const observeColors = isOn =>
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
: mo.disconnect();
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
let style, dup, md5, pageData, badKeys;
document.addEventListener('stylishInstallChrome', onClick);
document.addEventListener('stylishUpdateChrome', onClick);
runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
addEventListener(orphanEventId, orphanCheck, true);
addEventListener('click', onClick, true);
togglePageListener(true);
msg.on(onMessage);
[md5, dup] = await Promise.all([
fetch(md5Url).then(r => r.text()),
API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
.then(sendVarsToPage),
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
]);
let currentMd5;
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
Promise.all([
API.styles.find({md5Url}),
getResource(md5Url),
onDOMready(),
]).then(checkUpdatability);
if (!dup) {
sendStylishEvent('styleCanBeInstalledChrome');
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
sendStylishEvent('styleCanBeUpdatedChrome');
} else {
sendStylishEvent('styleAlreadyInstalledChrome');
}
document.documentElement.appendChild(
Object.assign(document.createElement('script'), {
textContent: `(${inPageContext})('${pageEventId}')`,
}));
async function onClick(e) {
for (const [sel, fn] of CLICK) {
const el = e.target.closest(sel);
if (!el) continue;
try {
el.disabled = true;
await fn(e);
} catch (e) {
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
} finally {
el.disabled = false;
}
function onMessage(msg) {
switch (msg.method) {
case 'ping':
// orphaned content script check
return true;
case 'openSettings':
openSettings();
return true;
}
}
function onCustomize() {
const ss = $('#style-settings');
const willShow = !ss || !ss.offsetHeight;
observeColors(willShow);
toggleListener(willShow, 'change', onChange);
/* since we are using "stylish-code-chrome" meta key on all browsers and
US.o does not provide "advanced settings" on this url if browser is not Chrome,
we need to fix this URL using "stylish-update-url" meta key
*/
function getStyleURL() {
const textUrl = getMeta('stylish-update-url') || '';
const jsonUrl = getMeta('stylish-code-chrome') ||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
}
async function onInstall(e) {
const {id} = dup;
e.stopPropagation();
if (!style) await buildStyle();
style = dup = await API.usercss.install(style, {
dup: {id},
vars: getPageVars(),
});
sendStylishEvent('styleInstalledChrome');
API.uso.pingback(id);
}
function checkUpdatability([installedStyle, md5]) {
// TODO: remove the following statement when USO is fixed
document.dispatchEvent(new CustomEvent(pageEventId, {
detail: installedStyle && installedStyle.updateUrl,
}));
currentMd5 = md5;
if (!installedStyle) {
sendEvent({type: 'styleCanBeInstalledChrome'});
return;
}
const isCustomizable = /\?/.test(installedStyle.updateUrl);
const md5Url = getMeta('stylish-md5-url');
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
reportUpdatable(isCustomizable || md5 !== installedStyle.originalMd5);
} else {
getStyleJson().then(json => {
reportUpdatable(
isCustomizable ||
!json ||
!styleSectionsEqual(json, installedStyle));
});
}
function onUninstall() {
const {id} = dup;
dup = style = false;
observeColors(false);
removeEventListener('change', onChange);
return API.styles.delete(id);
}
function prepareInstallButton() {
return new Promise(resolve => {
const observer = new MutationObserver(check);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
check();
function onChange({target: el}) {
if (dup && el.matches('[name^="ik-"], [type=file]')) {
API.usercss.configVars(dup.id, getPageVars());
function check() {
if (document.querySelector('#install_style_button')) {
resolve();
observer.disconnect();
}
}
});
}
function reportUpdatable(isUpdatable) {
prepareInstallButton().then(() => {
sendEvent({
type: isUpdatable
? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome',
detail: {
updateUrl: installedStyle.updateUrl,
},
});
});
}
}
function onMutation(mutations) {
for (const {target: el} of mutations) {
if (el.style.display === 'none' &&
/^ik-/.test(el.name) &&
/^#[\da-f]{6}$/.test(el.value)) {
onChange({target: el});
}
function sendEvent(event) {
sendEvent.lastEvent = event;
let {type, detail = null} = event;
if (typeof cloneInto !== 'undefined') {
// Firefox requires explicit cloning, however USO can't process our messages anyway
// because USO tries to use a global "event" variable deprecated in Firefox
detail = cloneInto({detail}, document); /* global cloneInto */
} else {
detail = {detail};
}
document.dispatchEvent(new CustomEvent(type, detail));
}
function onClick(event) {
if (onClick.processing || !orphanCheck()) {
return;
}
onClick.processing = true;
doInstall()
.then(() => {
if (!event.type.includes('Update')) {
// FIXME: sometimes the button is broken i.e. the button sends
// 'install' instead of 'update' event while the style is already
// install.
// This triggers an incorrect install count but we don't really care.
return getResource(getMeta('stylish-install-ping-url-chrome'));
}
})
.catch(console.error)
.then(done);
function done() {
setTimeout(() => {
onClick.processing = false;
});
}
}
function onPageEvent(e) {
pageData = e.detail;
togglePageListener(false);
function doInstall() {
let oldStyle;
return API.styles.find({
md5Url: getMeta('stylish-md5-url') || location.href,
})
.then(_oldStyle => {
oldStyle = _oldStyle;
return oldStyle ?
oldStyle.name :
getResource(getMeta('stylish-description'));
})
.then(name => {
const props = {};
if (oldStyle) {
props.id = oldStyle.id;
}
return saveStyleCode(oldStyle ? 'styleUpdate' : 'styleInstall', name, props);
});
}
async function buildStyle() {
if (!pageData) pageData = await (await fetch(apiUrl)).json();
({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
Object.assign(style, {
md5Url,
id: dup.id,
originalMd5: md5,
updateUrl: apiUrl,
});
}
async function saveStyleCode(message, name, addProps = {}) {
const isNew = message === 'styleInstall';
const needsConfirmation = isNew || !saveStyleCode.confirmed;
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
return Promise.reject();
}
saveStyleCode.confirmed = true;
enableUpdateButton(false);
const json = await getStyleJson();
if (!json) {
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
'https://github.com/openstyles/stylus/issues/195');
return;
}
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
const style = await API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}));
if (!isNew && style.updateUrl.includes('?')) {
enableUpdateButton(true);
} else {
sendEvent({type: 'styleInstalledChrome'});
}
function getPageVars() {
const {vars} = (style || dup).usercssData;
for (const el of document.querySelectorAll('[name^="ik-"]')) {
const name = el.name.slice(3); // dropping "ik-"
const ik = (badKeys || {})[name] || name;
const v = vars[ik] || false;
const isImage = el.type === 'radio';
if (v && (!isImage || el.checked)) {
const val = el.value;
const isFile = val === 'user-upload';
if (isImage && (isFile || val === 'user-url')) {
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
const ikCust = `${ik}-custom`;
v.value = `${ikCust}-dropdown`;
vars[ikCust].value = isFile ? getFileUriFromPage(el2) : el2.value;
} else {
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
function enableUpdateButton(state) {
const important = s => s.replace(/;/g, '!important;');
const button = document.getElementById('update_style_button');
if (button) {
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
const icon = button.querySelector('img[src*=".svg"]');
if (icon) {
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
if (state) {
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
}
}
}
}
return vars;
}
function getFileUriFromPage(el) {
togglePageListener(true);
sendPageEvent(el);
return pageData;
function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null;
}
function runInPage(fn, ...args) {
const div = document.createElement('div');
div.attachShadow({mode: 'closed'})
.appendChild(document.createElement('script'))
.textContent = `(${fn})(${JSON.stringify(args).slice(1, -1)})`;
document.documentElement.appendChild(div).remove();
}
function sendPageEvent(data) {
dispatchEvent(data instanceof Node
? new MouseEvent(pageEventId, {relatedTarget: data})
: new CustomEvent(pageEventId, {detail: data}));
//* global cloneInto */// WARNING! Firefox requires cloning of an object `detail`
}
function sendStylishEvent(type) {
document.dispatchEvent(new Event(type));
}
function sendVarsToPage(style) {
if (style) {
const vars = (style.usercssData || {}).vars || `${style.updateUrl}`.split('?')[1];
if (vars) sendPageEvent('vars:' + JSON.stringify(vars));
async function getResource(url, opts) {
try {
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);
}
}
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
// instead of "https://update.userstyles.org/#####.md5"
async function getStyleJson() {
try {
const style = await getResource(getStyleURL(), {responseType: 'json'});
const codeElement = document.getElementById('stylish-code');
if (!style || !Array.isArray(style.sections) || style.sections.length ||
codeElement && !codeElement.textContent.trim()) {
return style;
}
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) {}
}
/**
* The sections are checked in successive order because it matters when many sections
* match the same URL and they have rules with the same CSS specificity
* @param {Object} a - first style object
* @param {Object} b - second style object
* @returns {?boolean}
*/
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) {
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
return typeA && typeB && comparator(a, b) ||
(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));
}
}
function onDOMready() {
return document.readyState !== 'loading'
? Promise.resolve()
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
}
function openSettings(countdown = 10e3) {
const button = document.querySelector('.customize_button');
if (button) {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
setTimeout(function pollArea(countdown = 2000) {
const area = document.getElementById('advancedsettings_area');
if (area || countdown < 0) {
(area || button).scrollIntoView({behavior: 'smooth', block: area ? 'end' : 'center'});
} else {
setTimeout(pollArea, 100, countdown - 100);
}
}, 500);
} else if (countdown > 0) {
setTimeout(openSettings, 100, countdown - 100);
}
return style || false;
}
function orphanCheck() {
if (chrome.runtime.id) return true;
removeEventListener(orphanEventId, orphanCheck, true);
removeEventListener('click', onClick, true);
removeEventListener('change', onChange);
sendPageEvent('quit');
observeColors(false);
togglePageListener(false);
try {
if (chrome.i18n.getUILanguage()) {
return true;
}
} catch (e) {}
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
document.removeEventListener('stylishInstallChrome', onClick);
document.removeEventListener('stylishUpdateChrome', onClick);
try {
msg.off(onMessage);
} catch (e) {}
}
})();
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
let done, orphaned, vars;
// `chrome` may be empty if no extensions use externally_connectable but USO needs it
if (!window.chrome) window.chrome = {runtime: {sendMessage: () => {}}};
const EXT_ID = 'fjnbnpbmkenffdnngjfgmeleoegfcffe';
const {defineProperty} = Object;
const {dispatchEvent, CustomEvent, removeEventListener} = window;
const apply = Map.call.bind(Map.apply);
const OVR = [
[chrome.runtime, 'sendMessage', (fn, me, args) => {
const [id, /*msg*/, opts, cb = opts] = args;
if (id !== EXT_ID) return apply(fn, me, args);
if (typeof cb !== 'function') return Promise.resolve(true);
cb(true);
}],
[Response.prototype, 'json', async (fn, me, args) => {
const res = await apply(fn, me, args);
try {
if (!done && me.url === apiUrl) {
done = true;
send(res);
setVars(res);
}
} catch (e) {}
return res;
}],
[window, 'fetch', (fn, me, args) =>
args[0] === `chrome-extension://${EXT_ID}/index.html`
? Promise.resolve(new Response('<!doctype html><html lang="en"></html>'))
: apply(fn, me, args),
],
];
OVR.forEach(([obj, name, caller], i) => {
const orig = obj[name];
const ovr = new Proxy(orig, {
apply(fn, me, args) {
if (orphaned) restore(obj, name, ovr, fn);
return (orphaned ? apply : caller)(fn, me, args);
},
});
defineProperty(obj, name, {value: ovr});
OVR[i] = [obj, name, ovr, orig]; // same args as restore()
});
/* We set `isInstalled` at page start intentionally not trying to replicate Stylish login events.
* This difference allows USO site to detect presence of Stylus (or another similar extension). */
function inPageContext(eventId) {
document.currentScript.remove();
window.isInstalled = true;
addEventListener(eventId, onCommand, true);
function onCommand(e) {
if (e.detail === 'quit') {
removeEventListener(eventId, onCommand, true);
OVR.forEach(restore);
done = orphaned = true;
} else if (/^vars:/.test(e.detail)) {
vars = JSON.parse(e.detail.slice(5));
} else if (e.relatedTarget) {
send(e.relatedTarget.uploadedData);
}
}
function restore(obj, name, ovr, orig) { // same order as OVR after patching
if (obj[name] === ovr) {
defineProperty(obj, name, {value: orig});
}
}
function send(data) {
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
}
function setVars(json) {
const images = new Map();
const isNew = typeof vars === 'object';
const badKeys = {};
const newKeys = [];
const makeKey = ({install_key: key}) => {
let res = isNew ? badKeys[key] : key;
if (!res) {
res = key.replace(/[^-\w]/g, '-');
res += newKeys.includes(res) ? '-' : '';
if (key !== res) {
badKeys[key] = res;
newKeys.push(res);
const origMethods = {
json: Response.prototype.json,
byId: document.getElementById,
};
let vars;
// USO bug workaround: prevent errors in console after install and busy cursor
document.getElementById = id =>
origMethods.byId.call(document, id) ||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
: null);
// USO bug workaround: use the actual image data in customized settings
document.addEventListener(eventId, ({detail}) => {
vars = /\?/.test(detail) && new URL(detail).searchParams;
if (!vars) Response.prototype.json = origMethods.json;
}, {once: true});
Response.prototype.json = async function () {
const json = await origMethods.json.apply(this, arguments);
if (vars && json && Array.isArray(json.style_settings)) {
Response.prototype.json = origMethods.json;
const images = new Map();
for (const ss of json.style_settings) {
let value = vars.get('ik-' + ss.install_key);
if (!value || !(ss.style_setting_options || [])[0]) {
continue;
}
}
return res;
};
if (!isNew) vars = new URLSearchParams(vars);
for (const ss of json.style_settings || []) {
const ik = makeKey(ss);
let value = isNew ? (vars[ik] || {}).value : vars.get('ik-' + ik);
if (value == null || !(ss.style_setting_options || [])[0]) {
continue;
}
if (ss.setting_type === 'image') {
let isListed;
for (const opt of ss.style_setting_options) {
isListed |= opt.default = (opt.install_key === value);
}
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {
value = value.replace(/^ik-/, '');
const def = ss.style_setting_options.find(item => item.default);
if (!def || makeKey(def) !== value) {
if (def) def.default = false;
for (const item of ss.style_setting_options) {
if (makeKey(item) === value) {
item.default = true;
break;
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;
for (const opt of ss.style_setting_options) {
isListed |= opt.default = (opt.value === value);
}
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;
}
}
} else {
const item = ss.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (document.getElementById('style-settings')) {
observer.disconnect();
for (const [name, {url, isListed}] of images) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio &&
document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url;
}
}
}
}).observe(document, {childList: true, subtree: true});
}
}
if (!images.size) return;
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) return;
observer.disconnect();
for (const [name, {url, isListed}] of images) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
};
}

View File

@ -1,43 +0,0 @@
/* global API */// msg.js
'use strict';
(() => {
const ORIGIN = 'https://userstyles.world';
const HANDLERS = Object.assign(Object.create(null), {
async 'usw-ready'() {
send({type: 'usw-remove-stylus-button'});
if (location.pathname === '/api/oauth/style/new') {
const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
const data = await API.data.pop('usw' + styleId);
send({type: 'usw-fill-new-style', data});
}
},
async 'usw-style-info-request'(data) {
switch (data.requestType) {
case 'installed': {
const updateUrl = `${ORIGIN}/api/style/${data.styleID}.user.css`;
const style = await API.styles.find({updateUrl});
send({
type: 'usw-style-info-response',
data: {installed: Boolean(style), requestType: 'installed'},
});
break;
}
}
},
});
window.addEventListener('message', ({data, source, origin}) => {
// Some browsers don't reveal `source` to extensions e.g. Firefox
if (data && (source ? source === window : origin === ORIGIN)) {
const fn = HANDLERS[data.type];
if (fn) fn(data);
}
});
function send(msg) {
window.postMessage(msg, ORIGIN);
}
})();

View File

@ -9,14 +9,12 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
const PATCH_ID = 'transition-patch';
// styles are out of order if any of these elements is injected between them
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
const docRewriteObserver = RewriteObserver(sort);
const docRootObserver = RootObserver(sortIfNeeded);
const toSafeChar = c => String.fromCharCode(0xFF00 + c.charCodeAt(0) - 0x20);
const docRewriteObserver = RewriteObserver(_sort);
const docRootObserver = RootObserver(_sortIfNeeded);
const list = [];
const table = new Map();
let isEnabled = true;
let isTransitionPatched = chrome.app && CSS.supports('accent-color', 'red'); // Chrome 93
let exposeStyleName;
let isTransitionPatched;
// will store the original method refs because the page can override them
let creationDoc, createElement, createElementNS;
@ -25,24 +23,24 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
list,
async apply(styleMap) {
const styles = styleMapToArray(styleMap);
const styles = _styleMapToArray(styleMap);
const value = !styles.length
? []
: await docRootObserver.evade(() => {
if (!isTransitionPatched && isEnabled) {
applyTransitionPatch(styles);
_applyTransitionPatch(styles);
}
return styles.map(addUpdate);
return styles.map(_addUpdate);
});
emitUpdate();
_emitUpdate();
return value;
},
clear() {
addRemoveElements(false);
_addRemoveElements(false);
list.length = 0;
table.clear();
emitUpdate();
_emitUpdate();
},
clearOrphans() {
@ -55,12 +53,12 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
},
remove(id) {
remove(id);
emitUpdate();
_remove(id);
_emitUpdate();
},
replace(styleMap) {
const styles = styleMapToArray(styleMap);
const styles = _styleMapToArray(styleMap);
const added = new Set(styles.map(s => s.id));
const removed = [];
for (const style of list) {
@ -68,24 +66,22 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
removed.push(style.id);
}
}
styles.forEach(addUpdate);
removed.forEach(remove);
emitUpdate();
styles.forEach(_addUpdate);
removed.forEach(_remove);
_emitUpdate();
},
toggle(enable) {
if (isEnabled === enable) return;
isEnabled = enable;
if (!enable) toggleObservers(false);
addRemoveElements(enable);
if (enable) toggleObservers(true);
if (!enable) _toggleObservers(false);
_addRemoveElements(enable);
if (enable) _toggleObservers(true);
},
sort: sort,
};
function add(style) {
const el = style.el = createStyle(style);
function _add(style) {
const el = style.el = _createStyle(style.id, style.code);
const i = list.findIndex(item => compare(item, style) > 0);
table.set(style.id, style);
if (isEnabled) {
@ -95,7 +91,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
return el;
}
function addRemoveElements(add) {
function _addRemoveElements(add) {
for (const {el} of list) {
if (add) {
document.documentElement.appendChild(el);
@ -105,11 +101,11 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
}
}
function addUpdate(style) {
return table.has(style.id) ? update(style) : add(style);
function _addUpdate(style) {
return table.has(style.id) ? _update(style) : _add(style);
}
function applyTransitionPatch(styles) {
function _applyTransitionPatch(styles) {
isTransitionPatched = true;
// CSS transition bug workaround: since we insert styles asynchronously,
// the browsers, especially Firefox, may apply all transitions on page load
@ -118,20 +114,19 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
!styles.some(s => s.code.includes('transition'))) {
return;
}
const el = createStyle({id: PATCH_ID, code: `
const el = _createStyle(PATCH_ID, `
:root:not(#\\0):not(#\\0) * {
transition: none !important;
}
`});
`);
document.documentElement.appendChild(el);
// wait for the next paint to complete
// note: requestAnimationFrame won't fire in inactive tabs
requestAnimationFrame(() => setTimeout(() => el.remove()));
}
function createStyle(style = {}) {
const {id} = style;
if (!creationDoc) initCreationDoc();
function _createStyle(id, code = '') {
if (!creationDoc) _initCreationDoc();
let el;
if (document.documentElement instanceof SVGSVGElement) {
// SVG document style
@ -151,27 +146,18 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
el.type = 'text/css';
// SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus');
setTextAndName(el, style);
el.textContent = code;
return el;
}
function setTextAndName(el, {id, code = '', name}) {
if (exposeStyleName && name) {
el.dataset.name = name;
name = encodeURIComponent(name.replace(/[?#/']/g, toSafeChar));
code += `\n/*# sourceURL=${chrome.runtime.getURL(name)}.user.css#${id} */`;
}
el.textContent = code;
}
function toggleObservers(shouldStart) {
function _toggleObservers(shouldStart) {
const onOff = shouldStart && isEnabled ? 'start' : 'stop';
docRewriteObserver[onOff]();
docRootObserver[onOff]();
}
function emitUpdate() {
toggleObservers(list.length);
function _emitUpdate() {
_toggleObservers(list.length);
onUpdate();
}
@ -182,11 +168,11 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
and since userAgent.navigator can be spoofed via about:config or devtools,
we're checking for getPreventDefault that was removed in FF59
*/
function initCreationDoc() {
function _initCreationDoc() {
creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
if (creationDoc) {
({createElement, createElementNS} = creationDoc);
const el = document.documentElement.appendChild(createStyle());
const el = document.documentElement.appendChild(_createStyle());
const isApplied = el.sheet;
el.remove();
if (isApplied) return;
@ -195,7 +181,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
({createElement, createElementNS} = document);
}
function remove(id) {
function _remove(id) {
const style = table.get(id);
if (!style) return;
table.delete(id);
@ -203,14 +189,14 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
style.el.remove();
}
function sort() {
function _sort() {
docRootObserver.evade(() => {
list.sort(compare);
addRemoveElements(true);
_addRemoveElements(true);
});
}
function sortIfNeeded() {
function _sortIfNeeded() {
let needsSort;
let el = list.length && list[0].el;
if (!el) {
@ -231,29 +217,22 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
// some styles are not injected to the document
if (i < list.length) needsSort = true;
}
if (needsSort) sort();
if (needsSort) _sort();
return needsSort;
}
function styleMapToArray(styleMap) {
if (styleMap.cfg) {
({exposeStyleName} = styleMap.cfg);
delete styleMap.cfg;
}
return Object.values(styleMap).map(({id, code, name}) => ({
id,
name,
code: code.join(''),
function _styleMapToArray(styleMap) {
return Object.values(styleMap).map(s => ({
id: s.id,
code: s.code.join(''),
}));
}
function update(newStyle) {
const {id, code} = newStyle;
function _update({id, code}) {
const style = table.get(id);
if (style.code !== code ||
style.name !== newStyle.name && exposeStyleName) {
if (style.code !== code) {
style.code = code;
setTextAndName(style.el, newStyle);
style.el.textContent = code;
}
}
@ -262,14 +241,14 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
let root;
let observing = false;
let timer;
const observer = new MutationObserver(check);
const observer = new MutationObserver(_check);
return {start, stop};
function start() {
if (observing) return;
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
root = document.documentElement;
timer = setTimeout(check);
timer = setTimeout(_check);
observer.observe(document, {childList: true});
observing = true;
}
@ -281,7 +260,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
observing = false;
}
function check() {
function _check() {
if (root !== document.documentElement) {
root = document.documentElement;
onChange();
@ -311,7 +290,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
function evade(fn) {
const restore = observing && start;
stop();
return new Promise(resolve => run(fn, resolve, waitForRoot))
return new Promise(resolve => _run(fn, resolve, _waitForRoot))
.then(restore);
}
@ -329,7 +308,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
observing = false;
}
function run(fn, resolve, wait) {
function _run(fn, resolve, wait) {
if (document.documentElement) {
resolve(fn());
return true;
@ -337,8 +316,8 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
if (wait) wait(fn, resolve);
}
function waitForRoot(...args) {
new MutationObserver((_, observer) => run(...args) && observer.disconnect())
function _waitForRoot(...args) {
new MutationObserver((_, observer) => _run(...args) && observer.disconnect())
.observe(document, {childList: true});
}
}

250
edit.html
View File

@ -1,12 +1,22 @@
<!DOCTYPE html>
<html id="stylus">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="global.css" rel="stylesheet">
<link href="global-dark.css" rel="stylesheet">
<style id="cm-theme"></style>
<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">
<script src="js/polyfill.js"></script>
<script src="js/toolbox.js"></script>
@ -18,8 +28,6 @@
<script src="content/apply.js"></script>
<script src="js/sections-util.js"></script>
<script src="js/storage-util.js"></script>
<script src="edit/codemirror-themes.js"></script> <!-- must precede base.js -->
<script src="edit/base.js"></script>
<script src="vendor/codemirror/lib/codemirror.js"></script>
@ -41,18 +49,19 @@
<script src="vendor/codemirror/addon/lint/lint.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/keymap/emacs.js"></script>
<script src="vendor/codemirror/keymap/sublime.js"></script>
<script src="vendor/codemirror/keymap/vim.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="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>
<script src="edit/util.js"></script>
<script src="edit/codemirror-themes.js"></script>
<script src="edit/codemirror-default.js"></script>
<script src="edit/codemirror-factory.js"></script>
<script src="edit/moz-section-finder.js"></script>
@ -62,25 +71,25 @@
<script src="edit/source-editor.js"></script>
<script src="edit/sections-editor-section.js"></script>
<script src="edit/sections-editor.js"></script>
<script src="edit/usw-integration.js"></script>
<script src="edit/edit.js"></script>
<template data-id="appliesTo">
<li class="applies-to-item">
<div class="select-resizer">
<select name="applies-type" class="applies-type style-contributor">
<option value="url" i18n="appliesUrlOption"></option>
<option value="url-prefix" i18n="appliesUrlPrefixOption"></option>
<option value="domain" i18n="appliesDomainOption"></option>
<option value="regexp" i18n="appliesRegexpOption"></option>
<option value="url" i18n-text="appliesUrlOption"></option>
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
<option value="domain" i18n-text="appliesDomainOption"></option>
<option value="regexp" i18n-text="appliesRegexpOption"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<div class="applies-value-wrapper">
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
<a class="remove-applies-to" i18n="appliesRemove, title:appliesRemove" tabindex="0">
<a class="remove-applies-to" href="#" i18n-text="appliesRemove" i18n-title="appliesRemove">
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
</a>
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
<a class="add-applies-to" href="#" i18n-text="appliesAdd" i18n-title="appliesAdd">
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
</a>
</div>
@ -88,8 +97,8 @@
</template>
<template data-id="appliesToEverything">
<li class="applies-to-everything" i18n="appliesToEverything">
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
<li class="applies-to-everything" i18n-text="appliesToEverything">
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" href="#">
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
</a>
</li>
@ -99,25 +108,25 @@
<div class="section">
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
<p class="deleted-section">
<button class="restore-section" i18n="sectionRestore"></button>
<button class="restore-section" i18n-text="sectionRestore"></button>
</p>
<label i18n="sectionCode" class="code-label"></label>
<label i18n-text="sectionCode" class="code-label"></label>
<div class="applies-to">
<label i18n="appliesLabel, title:appliesHelp" data-cmd="note">
<a class="svg-inline-wrapper applies-to-help" tabindex="0">
<label i18n-text="appliesLabel">
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</label>
<ul class="applies-to-list"></ul>
</div>
<div class="edit-actions">
<button class="remove-section" i18n="sectionRemove"></button>
<button class="add-section" i18n="long-text:sectionAdd, short-text:genericAdd"></button>
<button class="clone-section" i18n="genericClone"></button>
<button class="remove-section" i18n-text="sectionRemove"></button>
<button class="add-section" i18n-long-text="sectionAdd" i18n-short-text="genericAdd"></button>
<button class="clone-section" i18n-text="genericClone"></button>
<button class="move-section-up"></button>
<button class="move-section-down"></button>
<button class="beautify-section" i18n="styleBeautify"></button>
<button class="test-regexp" i18n="genericTest"></button>
<button class="beautify-section" i18n-text="styleBeautify"></button>
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
</div>
</div>
</template>
@ -127,27 +136,27 @@
<div data-type="main">
<div data-type="content"></div>
<div data-type="actions">
<a data-action="case" i18n="title:searchCaseSensitive" tabindex="0">Aa</a>
<a data-action="prev" i18n="title:genericPrevious" data-hotkey-tooltip="findPrev" tabindex="0">
<a data-action="case" i18n-title="searchCaseSensitive" href="#" tabindex="0">Aa</a>
<a data-action="prev" i18n-title="genericPrevious" href="#" data-hotkey-tooltip="findPrev" tabindex="0">
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
</a>
<a data-action="next" i18n="title:genericNext" data-hotkey-tooltip="findNext" tabindex="0">
<a data-action="next" i18n-title="genericNext" href="#" data-hotkey-tooltip="findNext" tabindex="0">
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
</a>
<a data-action="close" i18n="title:confirmClose" data-hotkey-tooltip="=Esc" tabindex="0">
<a data-action="close" i18n-title="confirmClose" href="#" data-hotkey-tooltip="=Esc" tabindex="0">
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
</a>
</div>
</div>
<div data-type="status">
<div class="CodeMirror-search-hint" i18n-text="searchRegexp"></div>
<div data-type="tally" i18n="title:searchNumberOfResults"></div>
<div data-type="tally" i18n-title="searchNumberOfResults"></div>
</div>
</div>
</template>
<template data-id="clearSearch">
<div data-type="hover" i18n="title:confirmDelete">
<div data-type="hover" i18n-title="confirmDelete">
<svg data-action="clear" class="svg-icon"><use xlink:href="#svg-icon-close"></use></svg>
</div>
</template>
@ -156,7 +165,7 @@
<div data-type="content">
<div data-type="input-wrapper">
<textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required
i18n="placeholder:search"></textarea>
i18n-placeholder="search"></textarea>
</div>
</div>
</template>
@ -165,36 +174,36 @@
<div data-type="content">
<div data-type="input-wrapper">
<textarea data-type="replace-from"
i18n="placeholder:replace"
i18n-placeholder="replace"
class="CodeMirror-search-field" rows="1" required
spellcheck="false"></textarea>
</div>
<div data-type="input-wrapper">
<textarea data-type="replace-to"
i18n="placeholder:replaceWith"
i18n-placeholder="replaceWith"
class="CodeMirror-search-field" rows="1" required
spellcheck="false"></textarea>
</div>
<button data-action="replace" i18n="replace" disabled></button>
<button data-action="replaceAll" i18n="replaceAll" disabled></button>
<button data-action="undo" i18n="undo" disabled></button>
<button data-action="replace" i18n-text="replace" disabled></button>
<button data-action="replaceAll" i18n-text="replaceAll" disabled></button>
<button data-action="undo" i18n-text="undo" disabled></button>
<!--
Using a separate set of buttons because
1. FF can display tooltips only when specified on the <button>, ignores the nested <title> in <svg>
2. the icon doesn't fill the entire button area so tooltips aren't shown when the edges are hovered
-->
<button class="hidden" data-action="replace" i18n="title:replace" disabled>
<button class="hidden" data-action="replace" i18n-title="replace" disabled>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
</svg>
</button>
<button class="hidden" data-action="replaceAll" i18n="title:replaceAll" disabled>
<button class="hidden" data-action="replaceAll" i18n-title="replaceAll" disabled>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.8,1.8 8.8,8.8 5.2,5.3 3.5,6.9 8.8,12.2 17.5,3.4 "/>
<polygon points="15.8,7.8 8.8,14.8 5.2,11.3 3.5,12.9 8.8,18.2 17.5,9.4 "/>
</svg>
</button>
<button class="hidden" data-action="undo" i18n="title:undo" disabled>
<button class="hidden" data-action="undo" i18n-title="undo" disabled>
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M11.3,5.5H8.7V1.4L1.9,6.5l6.8,5.1V7.5h2.6c1.8,0,3.2,1.4,3.2,3.2s-1.4,3.2-3.2,3.2H7.8v2h3.5c2.9,0,5.2-2.3,5.2-5.2S14.2,5.5,11.3,5.5z"/>
</svg>
@ -203,7 +212,7 @@
</template>
<template data-id="jumpToLine">
<span i18n="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
</template>
<template data-id="regexpTestPartial">
@ -211,15 +220,15 @@
</template>
<template data-id="resizeGrip">
<div class="resize-grip" i18n="title:cm_resizeGripHint"></div>
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div>
</template>
<template data-id="keymapHelp">
<table class="keymap-list">
<thead>
<tr>
<th><input i18n="placeholder:helpKeyMapHotkey" type="search"></th>
<th><input i18n="placeholder:helpKeyMapCommand" type="search"></th>
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th>
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th>
</tr>
</thead>
<tbody>
@ -239,20 +248,16 @@
<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>
<template data-id="body"> <!-- https://crbug.com/1288447 -->
<body id="stylus-edit">
<div id="header">
<h1 id="heading" i18n="data-edit:editStyleHeading, data-add:addStyleTitle">
<a class="usercss-only"
href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n="title:externalUsercssDocument" target="_blank">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</h1>
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info">
<div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false">
<a id="reset-name" i18n="title:customNameResetHint" tabindex="0" hidden>
<a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
@ -262,99 +267,87 @@
</div>
<div id="basic-info-enabled">
<label id="enabled-label"
i18n="styleEnabledLabel, title:toggleStyle"
i18n-text="styleEnabledLabel"
i18n-title="toggleStyle"
data-hotkey-tooltip="toggleStyle">
<input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<label id="preview-label" i18n="previewLabel, title:previewTooltip">
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
<input type="checkbox" id="editor.livePreview">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<label id="disableAll-label" i18n="data-on:disableAllStyles, data-off:disableAllStylesOff">
<input id="disableAll" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<span id="preview-errors" hidden>!</span>
<span id="preview-errors" class="hidden">!</span>
</div>
</section>
<section id="actions">
<div class="buttons">
<div class="split-btn">
<button id="save-button" i18n="styleSaveLabel" data-hotkey-tooltip="save" disabled></button
><button class="split-btn-pedal usercss-only" i18n="menu-tpl:saveAsTemplate"></button>
</div>
<button id="beautify" i18n="styleBeautify"></button>
<button id="style-settings-btn" i18n="settings"></button>
<button id="cancel-button" i18n="title:styleCancelEditLabel"></button>
<div>
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button>
<button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div>
<div id="mozilla-format-buttons" class="buttons sectioned-only">
<button id="from-mozilla" i18n="importLabel"></button>
<button id="to-mozilla" i18n="exportLabel"></button>
<a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0"
i18n="title:styleMozillaFormatHeading">
<div id="mozilla-format-buttons" class="sectioned-only">
<button id="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button>
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0"
i18n-title="styleMozillaFormatHeading">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
</section>
<div id="details-wrapper">
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
<summary><h2 id="options-heading" i18n="editorSettings"></h2></summary>
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<div id="options-wrapper">
<div class="options-column">
<div class="option">
<label id="lineWrapping-label" i18n="cm_lineWrapping">
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
<input id="editor.lineWrapping" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label id="smartIndent-label" i18n="cm_smartIndent">
<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="cm_indentWithTabs">
<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="cm_autoCloseBrackets, title:cm_autoCloseBracketsTooltip">
<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="cm_autocompleteOnTyping">
<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="cm_selectByTokens, title:cm_selectByTokensTooltip">
<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 sectioned-only">
<label i18n="cm_arrowKeysTraverse">
<input id="editor.arrowKeysTraverse" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n="cm_colorpicker">
<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 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="appliesLineWidgetLabel, title:appliesLineWidgetWarning">
<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>
@ -362,89 +355,74 @@
</div>
<div class="options-column">
<div class="option aligned">
<label id="tabSize-label" for="editor.tabSize" i18n="cm_tabSize"></label>
<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="cm_keyMap"></label>
<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>
<a id="keyMap-help" class="svg-inline-wrapper" tabindex="0">
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
<div class="option aligned">
<label id="theme-label" for="editor.theme" i18n="cm_theme"></label>
<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="cm_matchHighlight"></label>
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
<div class="select-resizer">
<select id="editor.matchHighlight">
<option i18n="cm_matchHighlightToken" value="token">
<option i18n="cm_matchHighlightSelection" value="selection">
<option i18n="genericDisabledLabel" value="">
<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 class="option aligned">
<label id="linter-label" for="editor.linter" i18n="cm_linter"></label>
<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="genericDisabledLabel"></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 id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
</a>
</div>
</div>
</div>
</details>
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n="publish"></h2></summary>
<div>
<a id="usw-url" href="https://userstyles.world" target="_blank">&nbsp;</a>
<div id="usw-link-info">
<dl><dt i18n="styleName"></dt><dd data-usw="name"></dd></dl>
<dl><dt i18n="genericDescription"></dt><dd data-usw="description"></dd></dl>
</div>
<div>
<button id="usw-publish-style"
i18n="data-publish:publishStyle, data-push:publishPush"></button>
<button id="usw-disconnect" i18n="optionsSyncDisconnect"></button>
<span id="usw-progress"></span>
</div>
</div>
</details>
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n="sections"></h2></summary>
<summary><h2 i18n-text="sections"></h2></summary>
<ol id="toc"></ol>
</details>
<details id="lint" data-pref="editor.lint.expanded" class="ignore-pref-if-compact" hidden>
<details id="lint" data-pref="editor.lint.expanded" class="hidden-unless-compact ignore-pref-if-compact">
<summary>
<h2><span i18n="linterIssues"></span><span id="issue-count"></span>
<a id="lint-help" class="svg-inline-wrapper intercepts-click" tabindex="0">
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</h2>
</summary>
<div class="lint-report-container"></div>
<div class="lint-scroll-container">
<div class="lint-report-container"></div>
</div>
</details>
</div>
<div id="header-resizer" i18n="title:headerResizerHint"></div>
<div id="footer" class="hidden">
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n="externalUsercssDocument"
i18n-text="externalUsercssDocument"
target="_blank"></a>
</div>
</div>
@ -460,10 +438,8 @@
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
</symbol>
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n="alt:helpAlt">
<circle cx="7" cy="5" r="1"/>
<path d="M8,8c0-0.5-0.5-1-1-1H6C5.5,7,5,7.4,5,8h1v3c0,0.5,0.5,1,1,1h1c0.5,0,1-0.4,1-1H8V8z"/>
<path d="M7,1c3.9,0,7,3.1,7,7s-3.1,7-7,7s-7-3.1-7-7S3.1,1,7,1z M7,2.3C3.9,2.3,1.3,4.9,1.3,8s2.6,5.7,5.7,5.7s5.7-2.6,5.7-5.7S10.1,2.3,7,2.3C7,2.3,7,2.3,7,2.3z"/>
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n-alt="helpAlt">
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
</symbol>
<symbol id="svg-icon-close" viewBox="0 0 12 16">
@ -474,8 +450,8 @@
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
</symbol>
<symbol id="svg-icon-config" viewBox="0 0 16 16">
<path d="M13.3,12.8l1.5-2.6l-2.2-1.5c0-0.2,0.1-0.5,0.1-0.7c0-0.2,0-0.5-0.1-0.7l2.2-1.5l-1.5-2.6l-2.4,1.2 c-0.4-0.3-0.8-0.5-1.2-0.7L9.5,1h-3L6.3,3.7C5.9,3.8,5.5,4.1,5.1,4.4L2.7,3.2L1.2,5.8l2.2,1.5c0,0.2-0.1,0.5-0.1,0.7 c0,0.2,0,0.5,0.1,0.7l-2.2,1.5l1.5,2.6l2.4-1.2c0.4,0.3,0.8,0.5,1.2,0.7L6.5,15h3l0.2-2.7c0.4-0.2,0.8-0.4,1.2-0.7L13.3,12.8z M8,10.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,1-2.3,2.3-2.3c1.3,0,2.3,1,2.3,2.3C10.3,9.3,9.3,10.3,8,10.3z"/>
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"/>
</symbol>
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
@ -487,21 +463,13 @@
</symbol>
<symbol id="svg-icon-plus" viewBox="0 0 8 8">
<path d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
<path fill-rule="evenodd" d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
</symbol>
<symbol id="svg-icon-minus" viewBox="0 0 8 8">
<path d="M0 3v2h8v-2h-8z"/>
<path fill-rule="evenodd" d="M0 3v2h8v-2h-8z"/>
</symbol>
</svg>
</template>
<link href="edit/edit.css" rel="stylesheet">
<script src="js/dark-themer.js"></script> <!-- must be last in HEAD to avoid FOUC -->
</head>
<body id="stylus-edit">
<script src="edit/edit.js"></script>
</body>
</html>

View File

@ -2,7 +2,6 @@
/* global cmFactory */
/* global debounce */// toolbox.js
/* global editor */
/* global linterMan */
/* global prefs */
'use strict';
@ -12,7 +11,6 @@
const USO_VAR = 'uso-variable';
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
const USO_INVALID_VAR = 'error ' + USO_VAR;
const rxPROP = /^(prop(erty)?|variable-2|string-2)\b/;
const rxVAR = /(^|[^-.\w\u0080-\uFFFF])var\(/iyu;
const rxCONSUME = /([-\w]*\s*:\s?)?/yu;
const cssMime = CodeMirror.mimeModes['text/css'];
@ -20,28 +18,27 @@
const {tokenHooks} = cssMime;
const originalCommentHook = tokenHooks['/'];
const originalHelper = CodeMirror.hint.css || (() => {});
let cssMedia, cssProps, cssValues;
let cssProps, cssMedia;
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});
const aot = prefs.get('editor.autocompleteOnTyping');
CodeMirror.defineOption('autocompleteOnTyping', aot, aotToggled);
if (aot) cmFactory.globalSetOption('autocompleteOnTyping', true);
CodeMirror.registerHelper('hint', 'css', helper);
CodeMirror.registerHelper('hint', 'stylus', helper);
tokenHooks['/'] = tokenizeUsoVariables;
async function helper(cm) {
function aotToggled(cm, value) {
cm[value ? 'on' : 'off']('changes', autocompleteOnTyping);
cm[value ? 'on' : 'off']('pick', autocompletePicked);
}
function helper(cm) {
const pos = cm.getCursor();
const {line, ch} = pos;
const {styles, text} = cm.getLineHandle(line);
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
const isLessLang = cm.doc.mode.helperType === 'less';
const isStylusLang = cm.doc.mode.name === 'stylus';
const type = style && style.split(' ', 1)[0] || 'prop?';
if (!type || type === 'comment' || type === 'string') {
@ -67,7 +64,7 @@
const str = text.slice(prev, end);
const left = text.slice(prev, ch).trim();
let leftLC = left.toLowerCase();
let list;
let list = [];
switch (leftLC[0]) {
case '!':
@ -87,7 +84,6 @@
'@supports',
'@viewport',
];
if (isLessLang) list = findAllCssVars(cm, left, '\\s*:').concat(list);
break;
case '#': // prevents autocomplete for #hex colors
@ -124,36 +120,14 @@
/^(top|documentTypes|atBlock)/.test(getTokenState())) {
end++;
list = docFuncs;
break;
}
// fallthrough to `default`
break;
default:
// property values
if (isStylusLang || getTokenState() === 'prop') {
while (i > 0 && !rxPROP.test(styles[i + 1])) i -= 2;
const propEnd = styles[i];
let prop;
if (propEnd > text.lastIndexOf(';', ch - 1)) {
while (i > 0 && rxPROP.test(styles[i + 1])) i -= 2;
prop = text.slice(styles[i] || 0, propEnd).match(/([-\w]+)?$/u)[1];
}
if (prop) {
if (/[^-\w]/.test(leftLC)) {
prev += execAt(/[\s:()]*/y, prev, text)[0].length;
leftLC = leftLC.replace(/^[^\w\s]\s*/, '');
}
if (prop.startsWith('--')) prop = 'color'; // assuming 90% of variables are colors
if (!cssProps) await initCssProps();
list = [...new Set([...cssValues.all[prop] || [], ...cssValues.global])];
end = prev + execAt(/(\s*[-a-z(]+)?/y, prev, text)[0].length;
}
}
// properties and media features
if (!list &&
/^(prop(erty|\?)|atom|error|tag)/.test(type) &&
if (/^(prop(erty|\?)|atom|error)/.test(type) &&
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
if (!cssProps) await initCssProps();
if (!cssProps) initCssProps();
if (type === 'prop?') {
prev += leftLC.length;
leftLC = '';
@ -161,9 +135,7 @@
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) {
} else {
return isStylusLang
? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus})
: originalHelper(cm);
@ -176,15 +148,13 @@
};
}
async function initCssProps() {
cssValues = await linterMan.worker.getCssPropsValues();
cssProps = addSuffix(cssValues.all);
function initCssProps() {
cssProps = addSuffix(cssMime.propertyKeywords).sort();
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);
return Object.keys(obj).map(k => k + suffix);
}
function getMediaKeys([k, v]) {
@ -198,15 +168,13 @@
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
}
function findAllCssVars(cm, leftPart, rightPart = '') {
function findAllCssVars(cm, leftPart) {
// simplified regex without CSS escapes
const [, prefixed, named] = leftPart.match(/^(--|@)?(\S)?/);
const rx = new RegExp(
'(?:^|[\\s/;{])(' +
(prefixed ? leftPart : '--') +
(named ? '' : '[a-zA-Z_\u0080-\uFFFF]') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)' +
rightPart,
(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}) => {

View File

@ -1,13 +1,19 @@
/* global $$ $ $create messageBoxProxy setInputValue setupLivePrefs */// dom.js
/* global $ $$ $create setupLivePrefs waitForSelector */// dom.js
/* global API */// msg.js
/* global CODEMIRROR_THEMES */
/* global CodeMirror */
/* global MozDocMapper */// sections-util.js
/* global chromeSync */// storage-util.js
/* global initBeautifyButton */// beautify.js
/* global prefs */
/* global t */// localization.js
/* global FIREFOX getOwnTab sessionStore tryJSONparse tryURL */// toolbox.js
/* global
FIREFOX
debounce
getOwnTab
sessionStore
tryCatch
tryJSONparse
*/// toolbox.js
'use strict';
/**
@ -15,32 +21,15 @@
* @namespace Editor
*/
const editor = {
style: null,
dirty: DirtyReporter(),
isUsercss: false,
isWindowed: false,
livePreview: LivePreview(),
livePreview: null,
/** @type {'customName'|'name'} */
nameTarget: 'name',
previewDelay: 200, // Chrome devtools uses 200
saving: false,
scrollInfo: null,
cancel: () => location.assign('/manage.html'),
updateClass() {
$.rootCL.toggle('is-new-style', !editor.style.id);
},
updateTheme(name) {
if (!CODEMIRROR_THEMES[name]) {
name = 'default';
prefs.set('editor.theme', name);
}
$('#cm-theme').dataset.theme = name;
$('#cm-theme').textContent = CODEMIRROR_THEMES[name] || '';
},
updateTitle(isDirty = editor.dirty.isDirty()) {
const {customName, name} = editor.style;
document.title = `${
@ -53,22 +42,39 @@ const editor = {
//#region pre-init
(() => {
const mqCompact = matchMedia('(max-width: 850px)');
const toggleCompact = mq => $.rootCL.toggle('compact-layout', mq.matches);
mqCompact.on('change', toggleCompact);
toggleCompact(mqCompact);
Object.assign(editor, /** @namespace Editor */ {
mqCompact,
styleReady: prefs.ready.then(loadStyle),
});
const baseInit = (() => {
const lazyKeymaps = {
emacs: '/vendor/codemirror/keymap/emacs',
vim: '/vendor/codemirror/keymap/vim',
};
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([lazyKeymaps.emacs]) ||
/vim/i.test(km) && require([lazyKeymaps.vim]);
}
async function loadStyle() {
const params = new URLSearchParams(location.search);
let id = Number(params.get('id'));
const id = Number(params.get('id'));
const style = id && await API.styles.get(id) || {
id: id = null, // resetting the non-existent id
name: params.get('domain') ||
tryURL(params.get('url-prefix')).hostname ||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
'',
enabled: true,
sections: [
@ -76,37 +82,89 @@ const editor = {
],
};
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
const isUC = Boolean(style.usercssData || !id && prefs.get('newStyleAsUsercss'));
Object.assign(editor, /** @namespace Editor */ {
style,
isUsercss: isUC,
template: isUC && !id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate), // promise
});
editor.updateClass();
editor.updateTheme(prefs.get('editor.theme'));
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
editor.lazyKeymaps = lazyKeymaps;
editor.style = style;
editor.updateTitle(false);
$.rootCL.add(isUC ? 'usercss' : 'sectioned');
sessionStore.justEditedStyleId = id || '';
document.documentElement.classList.toggle('usercss', editor.isUsercss);
sessionStore.justEditedStyleId = style.id || '';
// no such style so let's clear the invalid URL parameters
if (!id) history.replaceState({}, '', location.pathname);
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 (theme !== 'default') {
const el = $('#cm-theme');
const el2 = await require([`/vendor/codemirror/theme/${theme}.css`]);
el2.id = el.id;
el.remove();
if (!el2.sheet) {
prefs.set('editor.theme', 'default');
}
}
}
})();
//#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 type of ['options', 'toc', 'lint']) {
const el = $(`details[data-pref="editor.${type}.expanded"]`);
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
/* exported EditorHeader */
function EditorHeader() {
baseInit.ready.then(() => {
initBeautifyButton($('#beautify'));
initKeymapElement();
initNameArea();
initThemeElement();
setupLivePrefs();
window.on('load', () => {
$('#heading').textContent = t(editor.style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#preview-label').classList.toggle('hidden', !editor.style.id);
require(Object.values(editor.lazyKeymaps), () => {
initKeymapElement();
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
}, {once: true});
});
function findKeyForCommand(command, map) {
if (typeof map === 'string') map = CodeMirror.keyMap[map];
@ -132,12 +190,19 @@ function EditorHeader() {
nameEl.title = isCustomName ? t('customNameHint') : '';
nameEl.on('input', () => {
editor.updateName(true);
resetEl.hidden = !editor.style.customName;
resetEl.hidden = false;
});
resetEl.hidden = !editor.style.customName;
resetEl.onclick = () => {
editor.style.customName = null; // to delete it from db
setInputValue(nameEl, editor.style.name);
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');
@ -147,7 +212,7 @@ function EditorHeader() {
function initThemeElement() {
$('#editor.theme').append(...[
$create('option', {value: 'default'}, t('defaultTheme')),
...Object.keys(CODEMIRROR_THEMES).map(s => $create('option', s)),
...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'));
@ -203,7 +268,7 @@ function EditorHeader() {
}
}
}
}
});
//#endregion
//#region init windowed mode
@ -220,17 +285,22 @@ function EditorHeader() {
}
}
getOwnTab().then(tab => {
getOwnTab().then(async tab => {
ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
editor.cancel = () => history.back();
await baseInit.domReady;
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
}
});
async function initWindowedMode() {
chrome.tabs.onAttached.addListener(onTabAttached);
// Chrome 96+ bug: the type is 'app' for a window that was restored via Ctrl-Shift-T
const isSimple = ['app', 'popup'].includes((await browser.windows.getCurrent()).type);
const isSimple = (await browser.windows.getCurrent()).type === 'popup';
if (isSimple) require(['/edit/embedded-popup']);
editor.isWindowed = isSimple || (
history.length === 1 &&
@ -266,15 +336,9 @@ function EditorHeader() {
function DirtyReporter() {
const data = new Map();
const listeners = new Set();
const dataListeners = new Set();
const notifyChange = wasDirty => {
const isDirty = data.size > 0;
const flipped = isDirty !== wasDirty;
if (flipped) {
listeners.forEach(cb => cb(isDirty));
}
if (flipped || isDirty) {
dataListeners.forEach(cb => cb(isDirty));
if (wasDirty !== (data.size > 0)) {
listeners.forEach(cb => cb());
}
};
/** @namespace DirtyReporter */
@ -291,19 +355,17 @@ function DirtyReporter() {
saved.newValue = value;
saved.type = 'modify';
}
} else {
return;
}
notifyChange(wasDirty);
},
clear(...objs) {
if (data.size && (
objs.length
? objs.map(data.delete, data).includes(true)
: (data.clear(), true)
)) {
notifyChange(true);
clear(obj) {
const wasDirty = data.size > 0;
if (obj === undefined) {
data.clear();
} else {
data.delete(obj);
}
notifyChange(wasDirty);
},
has(key) {
return data.has(key);
@ -317,8 +379,6 @@ function DirtyReporter() {
if (!saved) {
if (oldValue !== newValue) {
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
} else {
return;
}
} else if (saved.type === 'modify') {
if (saved.savedValue === newValue) {
@ -328,17 +388,12 @@ function DirtyReporter() {
}
} else if (saved.type === 'add') {
saved.newValue = newValue;
} else {
return;
}
notifyChange(wasDirty);
},
onChange(cb, add = true) {
listeners[add ? 'add' : 'delete'](cb);
},
onDataChange(cb, add = true) {
dataListeners[add ? 'add' : 'delete'](cb);
},
remove(obj, value) {
const wasDirty = data.size > 0;
const saved = data.get(obj);
@ -348,80 +403,10 @@ function DirtyReporter() {
data.delete(obj);
} else if (saved.type === 'modify') {
saved.type = 'remove';
} else {
return;
}
notifyChange(wasDirty);
},
};
}
function LivePreview() {
let el;
let data;
let port;
let preprocess;
let enabled = prefs.get('editor.livePreview');
prefs.subscribe('editor.livePreview', (key, value) => {
if (!value) {
if (port) {
port.disconnect();
port = null;
}
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
createPreviewer();
updatePreviewer(data);
}
enabled = value;
});
return {
/**
* @param {Function} [fn] - preprocessor
*/
init(fn) {
preprocess = fn;
},
update(newData) {
data = newData;
if (!port) {
if (!data.id || !data.enabled || !enabled) {
return;
}
createPreviewer();
}
updatePreviewer(data);
},
};
function createPreviewer() {
port = chrome.runtime.connect({name: 'livePreview'});
port.onDisconnect.addListener(err => {
throw err;
});
el = $('#preview-errors');
el.onclick = () => messageBoxProxy.alert(el.title, 'pre');
}
async function updatePreviewer(data) {
try {
port.postMessage(preprocess ? await preprocess(data) : data);
el.hidden = true;
} catch (err) {
if (Array.isArray(err)) {
err = err.map(e => e.message || e).join('\n');
} else if (err && err.index != null) {
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
const pos = editor.getEditors()[0].posFromIndex(err.index);
err.message = `${pos.line}:${pos.ch} ${err.message || err}`;
}
el.title = err.message || `${err}`;
el.hidden = false;
}
}
}
//#endregion

View File

@ -65,7 +65,7 @@ function beautifyEditor(cm, options, ui) {
window.scrollTo(scrollX, scrollY);
cm.beautifyChange[cm.changeGeneration()] = true;
if (ui) {
$('button[role="close"]', helpPopup.div).disabled = false;
$('#help-popup button[role="close"]').disabled = false;
}
}
}
@ -82,14 +82,10 @@ function createBeautifyUI(scope, options) {
$createOption('}', 'newline_between_rules'),
$createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'),
$createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'),
editor.isUsercss && $createLabeledCheckbox('indent_mozdoc', '', '... @-moz-document'),
]),
$create('p.beautify-hint', [
$create('span', t('styleBeautifyHint') + '\u00A0'),
createHotkeyInput('editor.beautify.hotkey', {
buttons: false,
onDone: () => moveFocus(helpPopup.div, 0),
}),
createHotkeyInput('editor.beautify.hotkey', () => moveFocus($('#help-popup'), 0)),
]),
$create('.buttons', [
$create('button', {
@ -114,16 +110,16 @@ function createBeautifyUI(scope, options) {
},
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
]),
]),
{
className: 'wide',
});
]));
$('#help-popup').className = 'wide';
$('.beautify-options').onchange = ({target}) => {
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
const elLine = target.closest('[newline]');
if (elLine) elLine.setAttribute('newline', value);
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
if (target.parentNode.hasAttribute('newline')) {
target.parentNode.setAttribute('newline', value.toString());
}
beautify(scope, false);
};
@ -149,7 +145,7 @@ function createBeautifyUI(scope, options) {
);
}
function $createLabeledCheckbox(optionName, i18nKey, text) {
function $createLabeledCheckbox(optionName, i18nKey) {
return (
$create('label', {style: 'display: block; clear: both;'}, [
$create('input', {
@ -159,7 +155,7 @@ function createBeautifyUI(scope, options) {
}),
$create('SVG:svg.svg-icon.checked',
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
i18nKey ? t(i18nKey) : text,
t(i18nKey),
])
);
}

View File

@ -4,23 +4,13 @@
z-index: 999;
}
.CodeMirror-hint:hover {
color: var(--bg);
color: white;
background: #08f;
}
.CodeMirror {
border: solid var(--c80) 1px;
border: solid #CCC 1px;
transition: box-shadow .1s;
}
.CodeMirror {
color: inherit;
background-color: inherit;
border: solid var(--c80) 1px;
transition: box-shadow .1s;
}
.CodeMirror-gutters {
background-color: var(--c95);
border-color: var(--c85);
}
#stylus#stylus .CodeMirror {
/* Using a specificity hack to override userstyles */
/* Not using the ring-color hack as it became ugly in new Chrome */
@ -36,7 +26,7 @@
width: 5em;
}
.CodeMirror-search-hint {
color: var(--c50);
color: #888;
}
.CodeMirror-activeline .applies-to:before {
background-color: hsla(214, 100%, 90%, 0.15);
@ -75,10 +65,6 @@
.CodeMirror-linenumber {
cursor: pointer; /* for bookmarking */
}
.cm-matchhighlight,
.CodeMirror-selection-highlight-scrollbar {
background: hsla(200, 100%, 50%, var(--match-hl-opacity, .1));
}
/* Custom stuff we add to CodeMirror */
@ -88,61 +74,3 @@
.gutter-bookmark {
background: linear-gradient(0deg, hsla(180, 100%, 30%, .75) 2px, hsla(180, 100%, 30%, .2) 2px);
}
@media screen and (prefers-color-scheme: dark), dark {
.CodeMirror {
--match-hl-opacity: .18;
}
.CodeMirror-dialog {
background-color: #333;
}
.CodeMirror-dialog-top {
border-color: #555;
}
.CodeMirror-activeline-background {
background: hsl(180, 21%, 18%);
}
.CodeMirror-selected,
.CodeMirror-focused .CodeMirror-selected,
.CodeMirror-line::selection,
.CodeMirror-line > span::selection,
.CodeMirror-line > span > span::selection {
background: #444;
}
.CodeMirror-line::-moz-selection,
.CodeMirror-line > span::-moz-selection,
.CodeMirror-line > span > span::-moz-selection {
/* TODO: remove this when strict_min_version >= 62 */
background: #444;
}
.cm-s-default div.CodeMirror-cursor {
border-left: 1px solid #fff;
}
/* Using Chromium's dark devtools colors */
.cm-s-default .cm-atom,
.cm-s-default .cm-number { color: #a1f7b5 }
.cm-s-default .cm-attribute { color: #6194c6 }
.cm-s-default .cm-bracket { color: #997 }
.cm-s-default .cm-builtin,
.cm-s-default .cm-link { color: #9fb4d6 }
.cm-s-default .cm-comment { color: #747474 }
.cm-s-default .cm-qualifier { color: #ffa34f }
.cm-s-default .cm-def,
.cm-s-default .cm-header,
.cm-s-default .cm-tag,
.cm-s-default .cm-type { color: #5db0d7 }
.cm-s-default .cm-hr { color: #999 }
.cm-s-default .cm-keyword { color: #9a7fd5 }
.cm-s-default .cm-meta { color: #ddfb55 }
.cm-s-default .cm-operator { color: #d2c057 }
.cm-s-default .cm-string { color: #f28b54 }
.cm-s-default .cm-variable { color: #d9d9d9 }
.cm-s-default .cm-variable-2 { color: #72b9ff }
.cm-s-default .cm-variable-3 { color: #9bbbdc }
@keyframes highlight {
from {
background-color: #888;
}
}
}

View File

@ -1,6 +1,5 @@
/* global $ */// dom.js
/* global CodeMirror */
/* global UA */// toolbox.js
/* global editor */
/* global prefs */
/* global t */// localization.js
@ -26,7 +25,7 @@
matchBrackets: true,
hintOptions: {},
lintReportDelay: prefs.get('editor.lintReportDelay'),
styleActiveLine: {nonEmpty: true},
styleActiveLine: true,
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap'),
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
@ -42,7 +41,7 @@
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
// Adding hotkeys to some keymaps except 'basic' which is primitive by design
{
require(Object.values(typeof editor === 'object' && editor.lazyKeymaps || {}), () => {
const KM = CodeMirror.keyMap;
const extras = Object.values(CodeMirror.defaults.extraKeys);
if (!extras.includes('jumpToLine')) {
@ -63,7 +62,7 @@
if (!extras.includes('blockComment')) {
KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
}
if (UA.windows) {
if (navigator.appVersion.includes('Windows')) {
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
@ -90,8 +89,33 @@
}
}
}
}
});
const cssMime = CodeMirror.mimeModes['text/css'];
Object.assign(cssMime.propertyKeywords, {
'content-visibility': true,
'overflow-anchor': true,
'overscroll-behavior': true,
});
Object.assign(cssMime.colorKeywords, {
'darkgrey': true,
'darkslategrey': true,
'dimgrey': true,
'lightgrey': true,
'lightslategrey': true,
'slategrey': true,
});
Object.assign(cssMime.valueKeywords, {
'blur': true,
'brightness': true,
'contrast': true,
'cubic-bezier': true,
'drop-shadow': true,
'fit-content': true,
'hue-rotate': true,
'saturate': true,
'sepia': true,
});
Object.assign(CodeMirror.prototype, {
/**
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
@ -102,7 +126,6 @@
const m = this.doc.mode;
if (force || (m.helperType ? m.helperType !== pp : m.name !== name)) {
this.setOption('mode', name);
this.doc.mode.lineComment = ''; // stylelint chokes on line comments a lot
}
},
/** Superfast GC-friendly check that runs until the first non-space line */

View File

@ -1,3 +1,4 @@
/* global $ */// dom.js
/* global CodeMirror */
/* global editor */
/* global prefs */
@ -31,7 +32,7 @@
globalSetOption(key, value) {
CodeMirror.defaults[key] = value;
if (cms.size > 4 && lazyOpt.names.includes(key)) {
if (cms.size > 4 && lazyOpt && lazyOpt.names.includes(key)) {
lazyOpt.set(key, value);
} else {
cms.forEach(cm => cm.setOption(key, value));
@ -39,18 +40,14 @@
},
};
// focus and blur
const onCmFocus = cm => {
rerouteHotkeys.toggle(false);
cm.display.wrapper.classList.add('CodeMirror-active');
cm.lastActive = Date.now();
};
const onCmBlur = cm => {
rerouteHotkeys.toggle(true);
setTimeout(() => {
/* Delaying to next tick to avoid double-processing of the currently processed keyboard event
* when it bubbles up from CodeMirror to `document` where the rerouter listens */
rerouteHotkeys.toggle(true);
const {wrapper} = cm.display;
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
});
@ -60,50 +57,36 @@
cm.on('blur', onCmBlur);
});
// propagated preferences
const prefToCmOpt = k =>
const handledPrefs = {
'editor.colorpicker'() {}, // handled in colorpicker-helper.js
async 'editor.theme'(key, value) {
let el2;
const el = $('#cm-theme');
if (value === 'default') {
el.href = '';
} else {
const path = `/vendor/codemirror/theme/${value}.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', value);
if (el2) {
el.remove();
el2.id = el.id;
}
},
};
const pref2opt = k => k.slice('editor.'.length);
const mirroredPrefs = prefs.knownKeys.filter(k =>
!handledPrefs[k] &&
k.startsWith('editor.') &&
k.slice('editor.'.length);
const prefKeys = prefs.knownKeys.filter(k =>
k !== 'editor.colorpicker' && // handled in colorpicker-helper.js
k !== 'editor.arrowKeysTraverse' && // handled in sections-editor.js
prefToCmOpt(k) in CodeMirror.defaults);
const {insertTab, insertSoftTab} = CodeMirror.commands;
Object.hasOwnProperty.call(CodeMirror.defaults, pref2opt(k)));
prefs.subscribe(mirroredPrefs, (k, val) => cmFactory.globalSetOption(pref2opt(k), val));
prefs.subscribeMany(handledPrefs);
for (const [key, fn] of Object.entries({
'editor.tabSize'(cm, value) {
cm.setOption('indentUnit', Number(value));
},
'editor.indentWithTabs'(cm, value) {
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
},
'editor.matchHighlight'(cm, value) {
const showToken = value === 'token' && /[#.\-\w]/;
const opt = (showToken || value === 'selection') && {
showToken,
annotateScrollbar: true,
delay: 0,
onUpdate: updateMatchHighlightCount,
};
cm.setOption('highlightSelectionMatches', opt || null);
},
'editor.selectByTokens'(cm, value) {
cm.setOption('configureMouse', value ? configureMouseFn : null);
},
})) {
CodeMirror.defineOption(prefToCmOpt(key), prefs.get(key), fn);
prefKeys.push(key);
}
prefs.subscribe(prefKeys, (key, val) => {
if (key === 'editor.theme') editor.updateTheme(val);
cmFactory.globalSetOption(prefToCmOpt(key), val);
});
// lazy propagation
lazyOpt = {
lazyOpt = window.IntersectionObserver && {
names: ['theme', 'lineWrapping'],
set(key, value) {
const {observer, queue} = lazyOpt;
@ -177,6 +160,30 @@
//#endregion
//#region CM option handlers
const {insertTab, insertSoftTab} = CodeMirror.commands;
Object.entries({
tabSize(cm, value) {
cm.setOption('indentUnit', Number(value));
},
indentWithTabs(cm, value) {
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
},
matchHighlight(cm, value) {
const showToken = value === 'token' && /[#.\-\w]/;
const opt = (showToken || value === 'selection') && {
showToken,
annotateScrollbar: true,
onUpdate: updateMatchHighlightCount,
};
cm.setOption('highlightSelectionMatches', opt || null);
},
selectByTokens(cm, value) {
cm.setOption('configureMouse', value ? configureMouseFn : null);
},
}).forEach(([name, fn]) => {
CodeMirror.defineOption(name, prefs.get('editor.' + name), fn);
});
function updateMatchHighlightCount(cm, state) {
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
}
@ -244,20 +251,7 @@
const BM_CLS = 'gutter-bookmark';
const BM_BRAND = 'sublimeBookmark';
const BM_CLICKER = 'CodeMirror-linenumbers';
const BM_DATA = Symbol('data');
// TODO: revisit when https://github.com/codemirror/CodeMirror/issues/6716 is fixed
const tmProto = CodeMirror.TextMarker.prototype;
const tmProtoOvr = {};
for (const k of ['clear', 'attachLine', 'detachLine']) {
tmProtoOvr[k] = function (line) {
const {cm} = this.doc;
const withOp = !cm.curOp;
if (withOp) cm.startOperation();
tmProto[k].apply(this, arguments);
cm.curOp.ownsGroup.delayedCallbacks.push(toggleMark.bind(this, this.lines[0], line));
if (withOp) cm.endOperation();
};
}
const {markText} = CodeMirror.prototype;
for (const name of ['prevBookmark', 'nextBookmark']) {
const cmdFn = CodeMirror.commands[name];
CodeMirror.commands[name] = cm => {
@ -269,9 +263,29 @@
CodeMirror.defineInitHook(cm => {
cm.on('gutterClick', onGutterClick);
cm.on('gutterContextMenu', onGutterContextMenu);
cm.on('markerAdded', onMarkAdded);
});
// TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
Object.assign(CodeMirror.prototype, {
markText() {
const marker = markText.apply(this, arguments);
if (marker[BM_BRAND]) {
this.doc.addLineClass(marker.lines[0], 'gutter', BM_CLS);
marker.clear = clearMarker;
}
return marker;
},
});
function clearMarker() {
const line = this.lines[0];
const spans = line.markedSpans;
delete this.clear; // removing our patch from the instance...
this.clear(); // ...and using the original prototype
if (!spans || spans.some(span => span.marker[BM_BRAND])) {
this.doc.removeLineClass(line, 'gutter', BM_CLS);
}
}
function onGutterClick(cm, line, name, e) {
switch (name === BM_CLICKER && e.button) {
case 0: {
@ -287,27 +301,13 @@
break;
}
}
function onGutterContextMenu(cm, line, name, e) {
if (name === BM_CLICKER) {
cm.execCommand(e.ctrlKey ? 'prevBookmark' : 'nextBookmark');
e.preventDefault();
}
}
function onMarkAdded(cm, mark) {
if (mark[BM_BRAND]) {
// CM bug workaround to keep the mark at line start when the above line is removed
mark.inclusiveRight = true;
Object.assign(mark, tmProtoOvr);
toggleMark.call(mark, true, mark[BM_DATA] = mark.lines[0]);
}
}
function toggleMark(state, line = this[BM_DATA]) {
this.doc[state ? 'addLineClass' : 'removeLineClass'](line, 'gutter', BM_CLS);
if (state) {
const bms = this.doc.cm.state.sublimeBookmarks;
if (!bms.includes(this)) bms.push(this);
}
}
//#endregion
})();

File diff suppressed because one or more lines are too long

View File

@ -1,68 +0,0 @@
/* global messageBoxProxy */// dom.js
/* global API */// msg.js
/* global clamp debounce */// toolbox.js
/* global editor */
/* global prefs */
/* global t */// localization.js
'use strict';
(async function AutosaveDrafts() {
const makeId = () => editor.style.id || 'new';
let delay;
let port;
connectPort();
const draft = await API.drafts.get(makeId());
if (draft && draft.isUsercss === editor.isUsercss) {
const date = makeRelativeDate(draft.date);
if (await messageBoxProxy.confirm(t('draftAction'), 'danger', t('draftTitle', date))) {
await editor.replaceStyle(draft.style, draft);
} else {
API.drafts.delete(makeId());
}
}
editor.dirty.onChange(isDirty => isDirty ? connectPort() : port.disconnect());
editor.dirty.onDataChange(isDirty => debounce(updateDraft, isDirty ? delay : 0));
prefs.subscribe('editor.autosaveDraft', (key, val) => {
delay = clamp(val * 1000 | 0, 1000, 2 ** 32 - 1);
const t = debounce.timers.get(updateDraft);
if (t != null) debounce(updateDraft, t ? delay : 0);
}, {runNow: true});
function connectPort() {
port = chrome.runtime.connect({name: 'draft:' + makeId()});
}
function makeRelativeDate(date) {
let delta = (Date.now() - date) / 1000;
if (delta >= 0 && Intl.RelativeTimeFormat) {
for (const [span, unit, frac = 1] of [
[60, 'second', 0],
[60, 'minute', 0],
[24, 'hour'],
[7, 'day'],
[4, 'week'],
[12, 'month'],
[1e99, 'year'],
]) {
if (delta < span) {
return new Intl.RelativeTimeFormat({style: 'short'}).format(-delta.toFixed(frac), unit);
}
delta /= span;
}
}
return date.toLocaleString();
}
function updateDraft(isDirty = editor.dirty.isDirty()) {
if (!isDirty) return;
API.drafts.put({
date: Date.now(),
isUsercss: editor.isUsercss,
style: editor.getValue(true),
si: editor.makeScrollInfo(),
}, makeId());
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,13 @@
/* global $$ $ $create */// dom.js
/* global $ $create messageBoxProxy waitForSheet */// dom.js
/* global API msg */// msg.js
/* global CodeMirror */
/* global SectionsEditor */
/* global SourceEditor */
/* global baseInit */
/* global clipString createHotkeyInput helpPopup */// util.js
/* global closeCurrentTab deepEqual mapObj sessionStore tryJSONparse */// toolbox.js
/* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
/* global cmFactory */
/* global editor EditorHeader */// base.js
/* global editor */
/* global linterMan */
/* global prefs */
/* global t */// localization.js
@ -14,29 +15,25 @@
//#region init
document.body.appendChild(t.template.body);
EditorMethods();
editor.styleReady.then(async () => {
EditorHeader();
dispatchEvent(new Event('domReady'));
await (editor.isUsercss ? SourceEditor : SectionsEditor)();
baseInit.ready.then(async () => {
await waitForSheet();
(editor.isUsercss ? SourceEditor : SectionsEditor)();
await editor.ready;
editor.ready = true;
editor.dirty.onChange(editor.updateDirty);
prefs.subscribe('editor.linter', () => linterMan.run());
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {runNow: true});
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
$('#name').required = !editor.isUsercss;
$('#save-button').onclick = editor.save;
$('#cancel-button').onclick = editor.cancel;
const elSec = $('#sections-list');
// editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work
if (elSec.open) editor.updateToc();
// and we also toggle `open` directly in other places e.g. in detectLayout()
new MutationObserver(() => elSec.open && editor.updateToc())
.observe(elSec, {attributes: true, attributeFilter: ['open']});
$('#sections-list').on('click', () => $('.compact-layout') && setTimeout(editor.updateToc),
{once: true});
$('#toc').onclick = e =>
editor.jumpToEditor([...$('#toc').children].indexOf(e.target));
$('#keyMap-help').onclick = () =>
@ -45,65 +42,20 @@ editor.styleReady.then(async () => {
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
$('#lint-help').onclick = () =>
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
$('#style-settings-btn').onclick = () => require([
'/edit/settings.css',
'/edit/settings', /* global StyleSettings */
], () => StyleSettings());
require([
'/edit/autocomplete',
'/edit/drafts',
'/edit/global-search',
]);
});
editor.styleReady.then(async () => {
// Set up mini-header on scroll
const {isUsercss} = editor;
const el = $create({
style: `
top: 0;
height: 1px;
position: absolute;
visibility: hidden;
`.replace(/;/g, '!important;'),
});
const scroller = isUsercss ? $('.CodeMirror-scroll') : document.body;
const xoRoot = isUsercss ? scroller : undefined;
const xo = new IntersectionObserver(onScrolled, {root: xoRoot});
scroller.appendChild(el);
onCompactToggled(editor.mqCompact);
editor.mqCompact.on('change', onCompactToggled);
/** @param {MediaQueryList} mq */
function onCompactToggled(mq) {
for (const el of $$('details[data-pref]')) {
el.open = mq.matches ? false : prefs.get(el.dataset.pref);
}
if (mq.matches) {
xo.observe(el);
} else {
xo.disconnect();
}
}
/** @param {IntersectionObserverEntry[]} entries */
function onScrolled(entries) {
const h = $('#header');
const sticky = !entries.pop().isIntersecting;
if (!isUsercss) scroller.style.paddingTop = sticky ? h.offsetHeight + 'px' : '';
h.classList.toggle('sticky', sticky);
}
});
//#endregion
//#region events
msg.onExtension(request => {
const {style} = request;
switch (request.method) {
case 'styleUpdated':
if (editor.style.id === style.id) {
handleExternalUpdate(request);
if (editor.style.id === style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
}
break;
case 'styleDeleted':
@ -111,47 +63,17 @@ msg.onExtension(request => {
closeCurrentTab();
}
break;
case 'editDeleteText':
document.execCommand('delete');
break;
}
});
async function handleExternalUpdate({style, reason}) {
if (reason === 'editPreview' ||
reason === 'editPreviewEnd') {
return;
}
if (reason === 'editSave' && editor.saving) {
editor.saving = false;
return;
}
if (reason === 'toggle') {
if (editor.dirty.isDirty()) {
editor.toggleStyle(style.enabled);
} else {
Object.assign(editor.style, style);
}
editor.updateMeta();
editor.updateLivePreview();
return;
}
style = await API.styles.get(style.id);
if (reason === 'config') {
delete style.sourceCode;
delete style.sections;
delete style.name;
delete style.enabled;
Object.assign(editor.style, style);
} else {
await editor.replaceStyle(style);
}
window.dispatchEvent(new Event('styleSettings'));
}
window.on('beforeunload', e => {
let pos;
if (editor.isWindowed &&
document.visibilityState === 'visible' &&
prefs.get('openEditInWindow') &&
screenX !== -32000 && // Chrome uses this value for minimized windows
( // only if not maximized
screenX > 0 || outerWidth < screen.availWidth ||
screenY > 0 || outerHeight < screen.availHeight ||
@ -168,7 +90,16 @@ window.on('beforeunload', e => {
prefs.set('windowPosition', pos);
}
sessionStore.windowPos = JSON.stringify(pos || {});
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify(editor.makeScrollInfo());
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;
if (activeElement) {
// blurring triggers 'change' or 'input' event if needed
@ -185,7 +116,7 @@ window.on('beforeunload', e => {
//#endregion
//#region editor methods
function EditorMethods() {
(() => {
const toc = [];
const {dirty} = editor;
let {style} = editor;
@ -207,37 +138,17 @@ function EditorMethods() {
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
if (si && si.sel) {
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
cm.setSelections(...si.sel, {scroll: false});
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
Object.assign(cm.display.scroller, si.scroll); // for source editor
Object.assign(cm.doc, si.scroll); // for sectioned editor
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));
});
}
},
makeScrollInfo() {
return {
scrollY: window.scrollY,
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
focus: cm.hasFocus(),
height: cm.display.wrapper.style.height.replace('100vh', ''),
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
scroll: mapObj(cm.doc, null, ['scrollLeft', 'scrollTop']),
sel: [cm.doc.sel.ranges, cm.doc.sel.primIndex],
})),
};
},
async save() {
if (dirty.isDirty()) {
editor.saving = true;
await editor.saveImpl();
}
},
toggleStyle(enabled = !style.enabled) {
$('#enabled').checked = enabled;
editor.updateEnabledness(enabled);
toggleStyle() {
$('#enabled').checked = !style.enabled;
editor.updateEnabledness(!style.enabled);
},
updateDirty() {
@ -303,18 +214,87 @@ function EditorMethods() {
el.classList.add(cls);
}
},
useSavedStyle(newStyle) {
if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
}
sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle);
editor.updateClass();
editor.updateMeta();
},
});
}
})();
//#endregion
//#region 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
* @param {boolean} [show]
*/
init(fn, show) {
preprocess = fn;
if (show != null) toggle(show);
},
toggle,
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;
});
}
function toggle(state) {
$('#preview-label').classList.toggle('hidden', !state);
}
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
@ -372,15 +352,17 @@ function EditorMethods() {
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 input = createHotkeyInput('editor.colorpicker.hotkey', () => 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();
input.focus();
};
function invokeColorpicker(cm) {

View File

@ -2,7 +2,9 @@
'use strict';
(() => {
let sugarss = false;
const hasCurlyBraceError = warning =>
warning.text === 'Unnecessary curly bracket (CssSyntaxError)';
let sugarssFallback;
/** @namespace EditorWorker */
createWorkerApi({
@ -14,38 +16,6 @@
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
},
getCssPropsValues() {
require(['/js/csslint/parserlib']); /* global parserlib */
const {
css: {Colors, GlobalKeywords, Properties},
util: {describeProp},
} = parserlib;
const namedColors = Object.keys(Colors);
const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g;
const res = {};
// moving vendor-prefixed props to the end
const cmp = (a, b) => a[0] === '-' && b[0] !== '-' ? 1 : a < b ? -1 : a > b;
for (const [k, v] of Object.entries(Properties)) {
res[k] = false;
if (typeof v === 'string') {
let last = '';
const uniq = [];
// strip definitions of function arguments
const desc = describeProp(v).replace(/([-\w]+)\(.*?\)/g, 'z-$1');
const descNoColors = desc.replace(/<named-color>/g, '');
// add a prefix to functions to group them at the end
const words = descNoColors.split(rxNonWord).sort(cmp);
for (let w of words) {
if (w.startsWith('z-')) w = w.slice(2) + '(';
if (w !== last) uniq.push(last = w);
}
if (desc !== descNoColors) uniq.push(...namedColors);
if (uniq.length) res[k] = uniq;
}
}
return {all: res, global: GlobalKeywords};
},
getRules(linter) {
return ruleRetriever[linter](); // eslint-disable-line no-use-before-define
},
@ -65,32 +35,23 @@
async stylelint(opts) {
require(['/vendor/stylelint-bundle/stylelint-bundle.min']); /* global stylelint */
// Stylus-lang allows a trailing ";" but sugarss doesn't, so we monkeypatch it
stylelint.SugarSSParser.prototype.checkSemicolon = tt => {
while (tt.length && tt[tt.length - 1][0] === ';') tt.pop();
};
for (const pass of opts.mode === 'stylus' ? [sugarss, !sugarss] : [-1]) {
/* We try sugarss (for indented stylus-lang), then css mode, switching them on failure,
* so that the succeeding syntax will be used next time first. */
opts.config.customSyntax = !pass ? 'sugarss' : '';
try {
const res = await stylelint.createLinter(opts)._lintSource(opts);
if (pass !== -1) sugarss = pass;
return collectStylelintResults(res, opts);
} catch (e) {
const fatal = pass === -1 ||
!pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e) ||
pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(`${e}${e.stack}`);
if (fatal) {
return [{
from: {line: e.line - 1, ch: e.column - 1},
to: {line: e.line - 1, ch: e.column - 1},
message: e.reason,
severity: 'error',
rule: e.name,
}];
}
}
try {
let res;
let pass = 0;
/* sugarss is used for stylus-lang by default,
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;
}
},
});
@ -145,32 +106,4 @@
return options;
},
};
function collectStylelintResults({messages}, {mode}) {
/* We hide nonfatal "//" warnings since we lint with sugarss without applying @preprocessor.
* We can't easily pre-remove "//" comments which may be inside strings, comments, url(), etc.
* And even if we did, it'd be wrong to hide potential bugs in stylus-lang like #1460 */
const isLess = mode === 'text/x-less';
const slashCommentAllowed = isLess || mode === 'stylus';
const res = [];
for (const m of messages) {
if (/deprecation|invalidOption/.test(m.stylelintType)) {
continue;
}
const {rule} = m;
const msg = m.text.replace(/^Unexpected\s+/, '').replace(` (${rule})`, '');
if (slashCommentAllowed && msg.includes('"//"') ||
isLess && /^unknown at-rule "@[-\w]+:"/.test(msg) /* LESS variables */) {
continue;
}
res.push({
from: {line: m.line - 1, ch: m.column - 1},
to: {line: m.endLine - 1, ch: m.endColumn - 1},
message: msg[0].toUpperCase() + msg.slice(1),
severity: m.severity,
rule,
});
}
return res;
}
})();

View File

@ -1,5 +1,6 @@
/* global $ $create $remove getEventKeyName */// dom.js
/* global CodeMirror */
/* global baseInit */// base.js
/* global prefs */
/* global t */// localization.js
'use strict';
@ -19,13 +20,12 @@
title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY,
onclick: embedPopup,
});
$.root.appendChild(btn);
$.rootCL.add('popup-window');
window.on('domReady', () => {
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';
}, {once: true});
});
prefs.subscribe('iconset', (_, val) => {
const prefix = `images/icon/${val ? 'light/' : ''}`;
@ -60,9 +60,14 @@
const body = pw.document.body;
pw.on('keydown', removePopupOnEsc);
pw.close = removePopup;
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
$create('div', {style: {height: '1px', marginTop: '-1px'}})
));
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'],

View File

@ -1,4 +1,4 @@
/* global $ $$ $create $remove focusAccessibility setInputValue toggleDataset */// dom.js
/* global $ $$ $create $remove focusAccessibility */// dom.js
/* global CodeMirror */
/* global chromeLocal */// storage-util.js
/* global colorMimicry */
@ -54,7 +54,7 @@
undoHistory: [],
searchInApplies: !editor.isUsercss,
searchInApplies: !document.documentElement.classList.contains('usercss'),
};
//endregion
@ -70,9 +70,7 @@
if (found) {
const target = $('.' + TARGET_CLASS);
const cm = target.CodeMirror;
/* Since this runs in `keydown` event we have to delay focusing
* to prevent CodeMirror from seeing and handling the key */
setTimeout(() => (cm || target).focus());
(cm || target).focus();
if (cm) {
const {from, to} = cm.state.search.searchPos;
cm.jumpToPos(from, to);
@ -588,7 +586,7 @@
input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
};
$.root.appendChild(
document.documentElement.appendChild(
$(DIALOG_STYLE_SELECTOR) ||
$create('style' + DIALOG_STYLE_SELECTOR)
).textContent = `
@ -607,10 +605,10 @@
}
#search-replace-dialog[data-type="replace"] button:hover svg,
#search-replace-dialog svg:hover {
fill: var(--cmin);
fill: inherit;
}
#search-replace-dialog [data-action="case"]:hover {
color: var(--cmin);
color: inherit;
}
#search-replace-dialog [data-action="clear"] {
background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'};
@ -680,7 +678,7 @@
el.style.width = newWidth + 'px';
}
const numLines = el.value.split('\n').length;
if (numLines !== Number(el.rows)) {
if (numLines !== parseInt(el.rows)) {
el.rows = numLines;
}
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
@ -876,6 +874,15 @@
}
function toggleDataset(el, prop, state) {
if (state) {
el.dataset[prop] = '';
} else {
delete el.dataset[prop];
}
}
function saveWindowScrollPos() {
state.scrollX = window.scrollX;
state.scrollY = window.scrollY;
@ -930,5 +937,18 @@
})));
}
function setInputValue(input, value) {
input.focus();
input.select();
// using execCommand to add to the input's undo history
document.execCommand(value ? 'insertText' : 'delete', false, value);
// some versions of Firefox ignore execCommand
if (input.value !== value) {
input.value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
}
}
//endregion
})();

View File

@ -17,10 +17,13 @@
let popup;
linterMan.showLintConfig = async () => {
linter = await getLinter();
linter = $('#editor.linter').value;
if (!linter) {
return;
}
if (!RULES[linter]) {
linterMan.worker.getRules(linter).then(res => (RULES[linter] = res));
}
await require([
'/vendor/codemirror/mode/javascript/javascript',
'/vendor/codemirror/addon/lint/json-lint',
@ -57,41 +60,28 @@
]));
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();
// FIXME: implement a linterChooser?
const linter = $('#editor.linter').value;
const baseUrl = linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
: '';
// some CSSLint rules do not have a url
: 'https://github.com/CSSLint/csslint/issues/535';
let headerLink, template;
if (linter === 'csslint') {
headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules', 'CSSLint');
template = ruleID => {
template = ({rule: 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),
$create('b', $createLink(rule.url || baseUrl, rule.name)),
$create('br'),
rule.desc,
]);
};
} else {
@ -101,20 +91,14 @@
rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
}
const header = t('linterIssuesHelp', '\x01').split('\x01');
const activeRules = new Set([...linterMan.getIssues()].map(issue => issue.rule));
helpPopup.show(t('linterIssues'),
$create([
header[0], headerLink, header[1],
$create('ul.rules', getActiveRules().map(template)),
$create('button', {onclick: linterMan.showLintConfig}, t('configureStyle')),
$create('ul.rules', [...activeRules].map(template)),
]));
};
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)) {
@ -123,14 +107,6 @@
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;

View File

@ -79,7 +79,7 @@ const linterMan = (() => {
function getCachedAnnotations(code, opt, cm) {
const results = cms.get(cm);
cms.set(cm, null);
cm.state.lint.options.getAnnotations = getAnnotations;
cm.options.lint.getAnnotations = getAnnotations;
return results;
}
@ -131,9 +131,7 @@ linterMan.DEFAULTS = {
'duplicate-properties': 1,
'empty-rules': 1,
'errors': 1,
'globals-in-document': 1,
'known-properties': 1,
'known-pseudos': 1,
'selector-newline': 1,
'shorthand-overrides': 1,
'simple-not': 1,
@ -202,7 +200,35 @@ linterMan.DEFAULTS = {
getConfig: config => ({
rules: Object.assign({}, DEFAULTS.stylelint.rules, config && config.rules),
}),
lint: (code, config, mode) => worker.stylelint({code, config, mode}),
async lint(code, config, mode) {
const isLess = mode === 'text/x-less';
const isStylus = mode === 'stylus';
const syntax = isLess ? 'less' : isStylus ? 'sugarss' : 'css';
const raw = await worker.stylelint({code, config, syntax});
if (!raw) {
return [];
}
// Hiding the errors about "//" comments as we're preprocessing only when saving/applying
// and we can't just pre-remove the comments since "//" may be inside a string token
const slashCommentAllowed = isLess || isStylus;
const res = [];
for (const w of raw.warnings) {
const msg = w.text.match(/^(?:Unexpected\s+)?(.*?)\s*\([^()]+\)$|$/)[1] || w.text;
if (!slashCommentAllowed || !(
w.rule === 'no-invalid-double-slash-comments' ||
w.rule === 'property-no-unknown' && msg.includes('"//"')
)) {
res.push({
from: {line: w.line - 1, ch: w.column - 1},
to: {line: w.line - 1, ch: w.column},
message: msg.slice(0, 1).toUpperCase() + msg.slice(1),
severity: w.severity,
rule: w.rule,
});
}
}
return res;
},
},
};
@ -287,7 +313,7 @@ linterMan.DEFAULTS = {
function updateCount() {
const issueCount = Array.from(tables.values())
.reduce((sum, table) => sum + table.trs.length, 0);
$('#lint').hidden = !issueCount;
$('#lint').classList.toggle('hidden-unless-compact', issueCount === 0);
$('#issue-count').textContent = issueCount;
}
@ -303,20 +329,19 @@ linterMan.DEFAULTS = {
}
function createTable(cm) {
const caption = $create('.caption');
const table = $create('table');
const report = $create('.report', [caption, table]);
const caption = $create('caption');
const tbody = $create('tbody');
const table = $create('table', [caption, tbody]);
const trs = [];
return {
element: report,
element: table,
trs,
updateAnnotations,
updateCaption,
};
function updateCaption() {
const t = editor.getEditorTitle(cm);
Object.assign(caption, typeof t == 'string' ? {textContent: t} : t);
caption.textContent = editor.getEditorTitle(cm);
}
function updateAnnotations(lines) {
@ -328,20 +353,20 @@ linterMan.DEFAULTS = {
} else {
tr = createTr();
trs.push(tr);
table.appendChild(tr.element);
tbody.append(tr.element);
}
tr.update(anno);
i++;
}
if (i === 0) {
trs.length = 0;
table.textContent = '';
tbody.textContent = '';
} else {
while (trs.length > i) {
trs.pop().element.remove();
}
}
report.classList.toggle('empty', !trs.length);
table.classList.toggle('empty', trs.length === 0);
function *getAnnotations() {
for (const line of lines.filter(Boolean)) {

View File

@ -4,7 +4,9 @@
/* global colorMimicry */
/* global editor */
/* global msg */
/* global prefs */
/* global t */// localization.js
/* global tryCatch */// toolbox.js
'use strict';
/* exported MozSectionWidget */
@ -38,12 +40,11 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
};
function init() {
const hint = {title: t('appliesHelp')};
enabled = true;
TPL = {
container:
$create('div' + C_CONTAINER, [
$create(C_LABEL, hint, t('appliesLabel')),
$create(C_LABEL, t('appliesLabel')),
$create('ul' + C_LIST),
]),
listItem:
@ -52,9 +53,8 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
$create('li.applies-to-everything', t('appliesToEverything')),
};
Object.assign($(C_TYPE, TPL.listItem), hint);
$(C_VALUE, TPL.listItem).after(
$create('button.test-regexp', t('genericTest')));
$create('button.test-regexp', t('styleRegexpTestButton')));
CLICK_ROUTE = {
'.test-regexp': showRegExpTester,
@ -129,8 +129,7 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
update(finder.sections, []);
}
finder.on(update);
updateWidgetStyle(); // updating in this paint frame to avoid FOUC for dark themes
cm.display.wrapper.style.setProperty('--cm-bar-width', cm.display.barWidth + 'px');
requestAnimationFrame(updateWidgetStyle);
}
function destroy() {
@ -156,7 +155,6 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
}
if (msg.style || msg.styles ||
msg.prefs && 'disableAll' in msg.prefs ||
msg.method === 'colorScheme' ||
msg.method === 'styleDeleted') {
requestAnimationFrame(updateWidgetStyle);
}
@ -164,6 +162,11 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
function updateWidgetStyle() {
funcHeight = 0;
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 = {
@ -199,6 +202,7 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
color: ${fore};
}
${C_CONTAINER} input,
${C_CONTAINER} button,
${C_CONTAINER} select {
background: rgba(255, 255, 255, ${
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
@ -212,7 +216,7 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
transition: none;
}
`;
$.root.appendChild(actualStyle);
document.documentElement.appendChild(actualStyle);
}
/**
@ -269,27 +273,19 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
let widget = old && old.widget;
const height = Math.round(funcHeight * (sec.funcs.length || 1)) || undefined;
const node = renderContainer(sec, widget);
if (widget && widget.line.lineNo() === sec.start.line) {
if (widget) {
widget.node = node;
if (height && height !== widget.height) {
widget.height = height;
widget.changed();
}
} else {
if (widget) widget.clear();
widget = cm.addLineWidget(sec.start.line, node, {
coverGutter: true,
noHScroll: true,
above: true,
height,
});
widget.on('redraw', () => {
const value = cm.display.barWidth + 'px';
if (widget[KEY] !== value) {
widget[KEY] = value;
node.style.setProperty('--cm-bar-width', value);
}
});
}
if (!funcHeight) {
funcHeight = node.offsetHeight / (sec.funcs.length || 1);

View File

@ -5,23 +5,24 @@
'use strict';
const regexpTester = (() => {
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const cachedRegexps = new Map();
let currentRegexps = [];
let isWatching = false;
let isShown = false;
window.on('closeHelp', () => regexpTester.toggle(false));
return {
toggle(state = !isShown) {
if (state && !isShown) {
if (!isWatching) {
isWatching = true;
chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onUpdated.addListener(onTabUpdated);
chrome.tabs.onUpdated.addListener(onTabUpdate);
}
helpPopup.show('', $create('.regexp-report'));
window.on('closeHelp', () => regexpTester.toggle(false), {once: true});
isShown = true;
} else if (!state && isShown) {
unwatch();
@ -92,15 +93,15 @@ const regexpTester = (() => {
for (const [url, match] of urls.entries()) {
const faviconUrl = url.startsWith(URLS.ownOrigin)
? OWN_ICON
: URLS.favicon(new URL(url).hostname);
: GET_FAVICON_URL + new URL(url).hostname;
const icon = $create('img', {src: faviconUrl});
if (match.text.length === url.length) {
full.push($create('a', {tabIndex: 0}, [
full.push($create('a', {href: '#'}, [
icon,
url,
]));
} else {
partial.push($create('a', {tabIndex: 0}, [
partial.push($create('a', {href: '#'}, [
icon,
url.substr(0, match.pos),
$create('mark', match.text),
@ -135,7 +136,8 @@ const regexpTester = (() => {
block.appendChild(
$create('details', {open: true}, [
$create('summary', text),
$create('div', urls),
// 3rd level: tab urls
...urls,
]));
} else {
// type is none or invalid
@ -152,26 +154,28 @@ const regexpTester = (() => {
.split(/(\\+)/)
.map(s => (s.startsWith('\\') ? $create('code', s) : s)));
report.appendChild(note);
report.style.paddingBottom = note.offsetHeight + 'px';
adjustNote(report, note);
},
};
function adjustNote(report, note) {
report.style.paddingBottom = note.offsetHeight + 'px';
}
function onClick(event) {
const a = event.target.closest('a, button');
const a = event.target.closest('a');
if (a) {
event.preventDefault();
openURL({
url: a.href || a.textContent,
url: a.href && a.getAttribute('href') !== '#' && a.href || a.textContent,
currentWindow: null,
});
} else if (event.target.closest('details')) {
setTimeout(adjustNote);
}
}
function onTabRemoved() {
regexpTester.update();
}
function onTabUpdated(tabId, info) {
function onTabUpdate(tabId, info) {
if (info.url) {
regexpTester.update();
}
@ -179,8 +183,7 @@ const regexpTester = (() => {
function unwatch() {
if (isWatching) {
chrome.tabs.onRemoved.removeListener(onTabRemoved);
chrome.tabs.onUpdated.removeListener(onTabUpdated);
chrome.tabs.onUpdated.removeListener(onTabUpdate);
isWatching = false;
}
}

View File

@ -1,4 +1,4 @@
/* global $ toggleDataset */// dom.js
/* global $ */// dom.js
/* global MozDocMapper trimCommentLabel */// util.js
/* global cmFactory */
/* global debounce tryRegExp */// toolbox.js
@ -23,7 +23,7 @@ function createSection(originalSection, genId, si) {
const elLabel = $('.code-label', el);
const cm = cmFactory.create(wrapper => {
// making it tall during initial load so IntersectionObserver sees only one adjacent CM
if (editor.loading) {
if (editor.ready !== true) {
wrapper.style.height = si ? si.height : '100vh';
}
elLabel.after(wrapper);
@ -31,13 +31,11 @@ function createSection(originalSection, genId, si) {
value: originalSection.code,
});
el.CodeMirror = cm; // used by getAssociatedEditor
cm.el = el;
editor.applyScrollInfo(cm, si);
const changeListeners = new Set();
const appliesToContainer = $('.applies-to', el);
const appliesToList = $('.applies-to-list', el);
const appliesToContainer = $('.applies-to-list', el);
const appliesTo = [];
MozDocMapper.forEachProp(originalSection, (type, value) =>
insertApplyAfter({type, value}));
@ -115,21 +113,66 @@ function createSection(originalSection, genId, si) {
changeGeneration = newGeneration;
emitSectionChange('code');
});
cm.display.wrapper.on('keydown', event => handleKeydown(cm, event), true);
$('.test-regexp', el).onclick = () => updateRegexpTester(true);
initBeautifyButton($('.beautify-section', el), [cm]);
}
function handleKeydown(cm, event) {
if (event.shiftKey || event.altKey || event.metaKey) {
return;
}
const {key} = event;
const {line, ch} = cm.getCursor();
switch (key) {
case 'ArrowLeft':
if (line || ch) {
return;
}
// fallthrough
case 'ArrowUp':
cm = line === 0 && editor.prevEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch);
break;
case 'ArrowRight':
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
return;
}
// fallthrough
case 'ArrowDown':
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(0, 0);
break;
}
}
async function updateRegexpTester(toggle) {
const isLoaded = typeof regexpTester === 'object' ||
toggle && await require(['/edit/regexp-tester']); /* global regexpTester */
if (toggle != null) {
const isLoaded = typeof regexpTester === 'object';
if (toggle && !isLoaded) {
await require(['/edit/regexp-tester']); /* global regexpTester */
}
if (toggle != null && isLoaded) {
regexpTester.toggle(toggle);
}
const regexps = appliesTo.filter(a => a.type === 'regexp')
.map(a => a.value);
const hasRe = regexps.length > 0;
if (hasRe && isLoaded) regexpTester.update(regexps);
el.classList.toggle('has-regexp', hasRe);
if (regexps.length) {
el.classList.add('has-regexp');
if (isLoaded) regexpTester.update(regexps);
} else {
el.classList.remove('has-regexp');
if (isLoaded) regexpTester.toggle(false);
}
}
function updateTocEntry(origin) {
@ -197,13 +240,11 @@ function createSection(originalSection, genId, si) {
function insertApplyAfter(init, base) {
const apply = createApply(init);
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
appliesToList.insertBefore(apply.el, base ? base.el.nextSibling : null);
toggleDataset(appliesToContainer, 'all', init.all);
appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null);
dirty.add(apply, apply);
if (appliesTo.length > 1 && appliesTo[0].all) {
removeApply(appliesTo[0]);
}
if (base) requestAnimationFrame(shrinkSectionBy1);
emitSectionChange('apply');
return apply;
}
@ -312,15 +353,6 @@ function createSection(originalSection, genId, si) {
dirty.add(`${dirtyPrefix}.value`, value);
}
}
function shrinkSectionBy1() {
const cmEl = cm.display.wrapper;
const cmH = cmEl.offsetHeight;
const viewH = el.parentElement.offsetHeight;
if (el.offsetHeight > viewH && cmH > Math.min(viewH / 2, cm.display.sizer.offsetHeight + 30)) {
cmEl.style.height = (cmH - appliesToContainer.offsetHeight / (appliesTo.length || 1) | 0) + 'px';
}
}
}
function createResizeGrip(cm) {
@ -349,7 +381,8 @@ function createResizeGrip(cm) {
cm.display.lineDiv.offsetParent.offsetTop +
/* borders */
wrapper.offsetHeight - wrapper.clientHeight;
document.body.classList.add('resizing-v');
wrapper.style.pointerEvents = 'none';
document.body.style.cursor = 's-resize';
document.on('mousemove', resize);
document.on('mouseup', resizeStop);
@ -365,7 +398,8 @@ function createResizeGrip(cm) {
function resizeStop() {
document.off('mouseup', resizeStop);
document.off('mousemove', resize);
document.body.classList.remove('resizing-v');
wrapper.style.pointerEvents = '';
document.body.style.cursor = '';
}
};

View File

@ -1,13 +1,12 @@
/* global $ $create $remove messageBoxProxy */// dom.js
/* global $ $$ $create $remove messageBoxProxy */// dom.js
/* global API */// msg.js
/* global CodeMirror */
/* global RX_META debounce */// toolbox.js
/* global FIREFOX URLS debounce ignoreChromeError sessionStore */// toolbox.js
/* global MozDocMapper clipString helpPopup rerouteHotkeys showCodeMirrorPopup */// util.js
/* global createSection */// sections-editor-section.js
/* global editor */
/* global linterMan */
/* global prefs */
/* global styleSectionsEqual */ // sections-util.js
/* global t */// localization.js
'use strict';
@ -17,25 +16,25 @@ function SectionsEditor() {
const container = $('#sections');
/** @type {EditorSection[]} */
const sections = [];
const xo = new IntersectionObserver(refreshOnViewListener, {rootMargin: '100%'});
const xo = window.IntersectionObserver &&
new IntersectionObserver(refreshOnViewListener, {rootMargin: '100%'});
let INC_ID = 0; // an increment id that is used by various object to track the order
let sectionOrder = '';
let headerOffset; // in compact mode the header is at the top so it reduces the available height
let cmExtrasHeight; // resize grip + borders
let upDownJumps;
updateMeta();
updateHeader();
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
editor.livePreview.init();
editor.livePreview.init(null, style.id);
container.classList.add('section-editor');
$('#to-mozilla').on('click', showMozillaFormat);
$('#to-mozilla-help').on('click', showToMozillaHelp);
$('#from-mozilla').on('click', () => showMozillaFormatImport());
document.on('wheel', scrollEntirePageOnCtrlShift, {passive: false});
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
prefs.subscribe('editor.arrowKeysTraverse', (_, val) => {
for (const {cm} of sections) handleKeydownSetup(cm, val);
upDownJumps = val;
}, {runNow: true});
if (!FIREFOX) {
$$('input:not([type]), input[type=text], input[type=search], input[type=number]')
.forEach(e => e.on('mousedown', toggleContextMenuDelete));
}
/** @namespace Editor */
Object.assign(editor, {
@ -44,23 +43,14 @@ function SectionsEditor() {
closestVisible,
updateLivePreview,
updateMeta,
getEditors() {
return sections.filter(s => !s.removed).map(s => s.cm);
},
getEditorTitle(cm) {
const index = editor.getEditors().indexOf(cm) + 1;
return {
textContent: `#${index}`,
title: `${t('sectionCode')} ${index}`,
};
},
getValue(asObject) {
const st = getModel();
return asObject ? st : MozDocMapper.styleToCss(st);
const index = editor.getEditors().indexOf(cm);
return `${t('sectionCode')} ${index + 1}`;
},
getSearchableInputs(cm) {
@ -76,62 +66,62 @@ function SectionsEditor() {
}
},
nextEditor(cm, upDown) {
return !upDown || cm !== findLast(sections, s => !s.removed).cm
? nextPrevEditor(cm, 1, upDown)
nextEditor(cm, cycle = true) {
return cycle || cm !== findLast(sections, s => !s.removed).cm
? nextPrevEditor(cm, 1)
: null;
},
prevEditor(cm, upDown) {
return !upDown || cm !== sections.find(s => !s.removed).cm
? nextPrevEditor(cm, -1, upDown)
prevEditor(cm, cycle = true) {
return cycle || cm !== sections.find(s => !s.removed).cm
? nextPrevEditor(cm, -1)
: null;
},
async replaceStyle(newStyle, draft) {
const sameCode = styleSectionsEqual(newStyle, getModel());
if (!sameCode && !draft && !await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
return;
}
if (!draft) {
dirty.clear();
}
async replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
// FIXME: avoid recreating all editors?
if (!sameCode) {
await initSections(newStyle.sections, {
keepDirty: draft,
replace: true,
si: draft && draft.si,
});
if (codeIsUpdated !== false) {
await initSections(newStyle.sections, {replace: true});
}
editor.useSavedStyle(newStyle);
Object.assign(style, newStyle);
updateHeader();
dirty.clear();
// Go from new style URL to edit style URL
if (location.href.indexOf('id=') === -1 && style.id) {
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
$('#heading').textContent = t('editStyleHeading');
}
editor.livePreview.toggle(Boolean(style.id));
updateLivePreview();
},
async saveImpl() {
async save() {
if (!dirty.isDirty()) {
return;
}
let newStyle = getModel();
if (!validate(newStyle)) {
return;
}
newStyle = await API.styles.editSave(newStyle);
dirty.clear();
editor.useSavedStyle(newStyle);
destroyRemovedSections();
sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false);
},
scrollToEditor(cm, partial) {
const cc = partial && cm.cursorCoords(true, 'window');
const {top: y1, bottom: y2} = cm.el.getBoundingClientRect();
const rc = container.getBoundingClientRect();
const rcY1 = Math.max(rc.top, 0);
const rcY2 = Math.min(rc.bottom, innerHeight);
const bad = partial
? cc.top < rcY1 || cc.top > rcY2 - 30
: y1 >= rcY1 ^ y2 <= rcY2;
if (bad) window.scrollBy(0, (y1 + y2 - rcY2 + rcY1) / 2 | 0);
scrollToEditor(cm) {
const {el} = sections.find(s => s.cm === cm);
const r = el.getBoundingClientRect();
const h = window.innerHeight;
if (r.bottom > h && r.top > 0 ||
r.bottom < h && r.top < 0) {
window.scrollBy(0, (r.top + r.bottom - h) / 2 | 0);
}
},
});
return initSections(style.sections);
editor.ready = initSections(style.sections);
/** @param {EditorSection} section */
function fitToContent(section) {
@ -148,17 +138,13 @@ function SectionsEditor() {
return;
}
if (headerOffset == null) {
headerOffset = Math.ceil(container.getBoundingClientRect().top + scrollY);
headerOffset = container.getBoundingClientRect().top + scrollY | 0;
}
if (cmExtrasHeight == null) {
cmExtrasHeight = $('.resize-grip', wrapper).offsetHeight + // grip
wrapper.offsetHeight - wrapper.clientHeight; // borders
}
contentHeight += cmExtrasHeight;
contentHeight += 9; // border & resize grip
cm.off('update', resize);
const cmHeight = wrapper.offsetHeight;
const appliesToHeight = Math.min(section.el.offsetHeight - cmHeight, window.innerHeight / 2);
const maxHeight = Math.floor(window.innerHeight - headerOffset - appliesToHeight);
const maxHeight = (window.innerHeight - headerOffset) - appliesToHeight;
const fit = Math.min(contentHeight, maxHeight);
if (Math.abs(fit - cmHeight) > 1) {
cm.setSize(null, fit);
@ -299,36 +285,10 @@ function SectionsEditor() {
}
}
function handleKeydown(event) {
if (event.shiftKey || event.altKey || event.metaKey ||
event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
return;
}
let pos;
let cm = this.CodeMirror;
const {line, ch} = cm.getCursor();
if (event.key === 'ArrowUp') {
cm = line === 0 && editor.prevEditor(cm, true);
pos = cm && [cm.doc.size - 1, ch];
} else {
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, true);
pos = cm && [0, 0];
}
if (cm) {
cm.setCursor(...pos);
event.preventDefault();
event.stopPropagation();
}
}
function handleKeydownSetup(cm, state) {
cm.display.wrapper[state ? 'on' : 'off']('keydown', handleKeydown, true);
}
function nextPrevEditor(cm, direction, upDown) {
function nextPrevEditor(cm, direction) {
const editors = editor.getEditors();
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
editor.scrollToEditor(cm, upDown);
editor.scrollToEditor(cm);
cm.focus();
return cm;
}
@ -358,7 +318,7 @@ function SectionsEditor() {
function showMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
popup.codebox.setValue(editor.getValue());
popup.codebox.setValue(MozDocMapper.styleToCss(getModel()));
popup.codebox.execCommand('selectAll');
}
@ -400,7 +360,7 @@ function SectionsEditor() {
lockPageUI(true);
try {
const code = popup.codebox.getValue().trim();
if (!RX_META.test(code) ||
if (!URLS.rxMETA.test(code) ||
!await getPreprocessor(code) ||
await messageBoxProxy.confirm(
t('importPreprocessor'), 'pre-line',
@ -430,7 +390,7 @@ function SectionsEditor() {
}
function lockPageUI(locked) {
$.root.style.pointerEvents = locked ? 'none' : '';
document.documentElement.style.pointerEvents = locked ? 'none' : '';
if (popup.codebox) {
popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
popup.codebox.options.readOnly = locked;
@ -460,7 +420,7 @@ function SectionsEditor() {
editor.updateToc();
}
/** @returns {StyleObj} */
/** @returns {Style} */
function getModel() {
return Object.assign({}, style, {
sections: sections.filter(s => !s.removed).map(s => s.getModel()),
@ -486,7 +446,19 @@ function SectionsEditor() {
return true;
}
function updateMeta() {
function destroyRemovedSections() {
for (let i = 0; i < sections.length;) {
if (!sections[i].removed) {
i++;
continue;
}
sections[i].destroy();
sections[i].el.remove();
sections.splice(i, 1);
}
}
function updateHeader() {
$('#name').value = style.customName || style.name || '';
$('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || '';
@ -504,15 +476,14 @@ function SectionsEditor() {
async function initSections(src, {
focusOn = 0,
replace = false,
keepDirty = false,
si = editor.scrollInfo,
keepDirty = false, // used by import
} = {}) {
Object.assign(editor, /** @namespace Editor */ {loading: true});
if (replace) {
sections.forEach(s => s.remove(true));
sections.length = 0;
container.textContent = '';
}
let si = editor.scrollInfo;
if (si && si.cms && si.cms.length === src.length) {
si.scrollY2 = si.scrollY + window.innerHeight;
container.style.height = si.scrollY2 + 'px';
@ -540,12 +511,9 @@ function SectionsEditor() {
if (!keepDirty) dirty.clear();
if (i === focusOn) sections[i].cm.focus();
}
if (!si || si.cms.every(cm => !cm.height)) {
requestAnimationFrame(fitToAvailableSpace);
}
if (!si) requestAnimationFrame(fitToAvailableSpace);
container.style.removeProperty('height');
setGlobalProgress();
editor.loading = false;
}
/** @param {EditorSection} section */
@ -611,9 +579,6 @@ function SectionsEditor() {
cm.focus();
editor.scrollToEditor(cm);
}
if (upDownJumps) {
handleKeydownSetup(cm, true);
}
updateSectionOrder();
updateLivePreview();
section.onChange(updateLivePreview);
@ -646,6 +611,7 @@ function SectionsEditor() {
/** @param {EditorSection} section */
function registerEvents(section) {
const {el, cm} = section;
$('.applies-to-help', el).onclick = () => helpPopup.show(t('appliesLabel'), t('appliesHelp'));
$('.remove-section', el).onclick = () => removeSection(section);
$('.add-section', el).onclick = () => insertSectionAfter(undefined, section);
$('.clone-section', el).onclick = () => insertSectionAfter(section.getModel(), section);
@ -653,13 +619,16 @@ function SectionsEditor() {
$('.move-section-down', el).onclick = () => moveSectionDown(section);
$('.restore-section', el).onclick = () => restoreSection(section);
cm.on('paste', maybeImportOnPaste);
if (!FIREFOX) {
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
}
}
function maybeImportOnPaste(cm, event) {
const text = event.clipboardData.getData('text') || '';
if (/@-moz-document/i.test(text) &&
/@-moz-document\s+(url|url-prefix|domain|regexp)\(/i
.test(text.replace(/\/\*([^*]+|\*(?!\/))*(\*\/|$)/g, ''))
.test(text.replace(/\/\*([^*]|\*(?!\/))*(\*\/|$)/g, ''))
) {
event.preventDefault();
showMozillaFormatImport(text);
@ -670,7 +639,7 @@ function SectionsEditor() {
if (code) {
linterMan.enableForEditor(cm, code);
}
if (force) {
if (force || !xo) {
refreshOnViewNow(cm);
} else {
xo.observe(cm.display.wrapper);
@ -697,4 +666,15 @@ function SectionsEditor() {
linterMan.enableForEditor(cm);
cm.refresh();
}
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);
}
}
}

View File

@ -1,47 +0,0 @@
#help-popup.style-settings-popup.dirty .title::after {
content: ' *';
}
.compact-layout #help-popup.style-settings-popup {
width: 90%;
}
.style-settings {
padding: 0 1px; /* for focus outline */
border: 0;
margin: 0;
}
.style-settings > * {
display: block;
margin: 1rem 0;
padding: 0;
}
.style-settings > :first-child {
margin-top: 0;
}
.style-settings > :last-child {
margin-bottom: 0;
}
.style-settings input:disabled ~ label {
opacity: .5;
}
.style-settings .w100 {
display: block;
width: 100%;
margin-top: .25em;
box-sizing: border-box;
}
.style-settings textarea {
resize: vertical;
min-width: 33vw;
min-height: 2.5em;
max-height: 50vh;
}
.style-settings textarea:not(:placeholder-shown) {
min-width: 50vw;
}
.style-settings .radio-wrapper {
display: inline-flex;
padding: 0 .8em 0 0;
}
a[data-cmd=note] {
vertical-align: text-bottom;
}

View File

@ -1,40 +0,0 @@
<div>
<fieldset class="style-settings">
<div class="rel">
<input id="ss-updatable" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<label i18n="installUpdateFromLabel" for="ss-updatable"></label>
<input id="ss-update-url" type="url" class="w100" i18n="placeholder:styleUpdateUrlLabel">
</div>
<div id="ss-scheme">
<div i18n="preferScheme">
<div><small id="ss-scheme-off" i18n="preferSchemeAlways" hidden></small></div>
</div>
<label i18n="+preferSchemeNone" class="radio-wrapper">
<input name="ss-scheme" type="radio" value="none">
</label>
<label i18n="+preferSchemeDark" class="radio-wrapper">
<input name="ss-scheme" type="radio" value="dark">
</label>
<label i18n="+preferSchemeLight" class="radio-wrapper">
<input name="ss-scheme" type="radio" value="light">
</label>
</div>
<label i18n="styleIncludeLabel">
<textarea id="ss-inclusions" spellcheck="false" class="w100"
placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
<label i18n="styleExcludeLabel">
<textarea id="ss-exclusions" spellcheck="false" class="w100"
placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
</fieldset>
<div class="buttons">
<button id="ss-save" i18n="confirmSave" disabled></button>
<label i18n="+configOnChange, title:configOnChangeTooltip">
<input id="config.autosave" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<button id="ss-close" i18n="confirmClose"></button>
</div>
</div>

View File

@ -1,124 +0,0 @@
/* global $ moveFocus setupLivePrefs */// dom.js
/* global API */// msg.js
/* global editor */
/* global helpPopup */// util.js
/* global prefs */
/* global t */// localization.js
/* global debounce tryURL */// toolbox.js
'use strict';
/* exported StyleSettings */
async function StyleSettings() {
const AUTOSAVE_DELAY = 500; // same as config-dialog.js
const SS_ID = 'styleSettings';
const PASS = val => val;
await t.fetchTemplate('/edit/settings.html', SS_ID);
const {style} = editor;
const ui = t.template[SS_ID].cloneNode(true);
const elAuto = $('#config\\.autosave', ui);
const elSave = $('#ss-save', ui);
const elUpd = $('#ss-updatable', ui);
const pendingSetters = new Map();
const updaters = [
initCheckbox(elUpd, 'updatable', tryURL(style.updateUrl).href),
initInput('#ss-update-url', 'updateUrl', '', {
validate(el) {
elUpd.disabled = !el.value || !el.validity.valid;
return el.validity.valid;
},
}),
initRadio('ss-scheme', 'preferScheme', 'none'),
initArea('inclusions'),
initArea('exclusions'),
];
update();
prefs.subscribe('schemeSwitcher.enabled', (_, val) => {
$('#ss-scheme-off', ui).hidden = val !== 'never';
}, {runNow: true});
window.on(SS_ID, update);
window.on('closeHelp', () => window.off(SS_ID, update), {once: true});
helpPopup.show(t(SS_ID), ui, {
className: 'style-settings-popup',
});
elSave.onclick = save;
$('#ss-close', ui).onclick = helpPopup.close;
setupLivePrefs([elAuto.id]);
moveFocus(ui, 0);
function autosave(el, setter) {
pendingSetters.set(el, setter);
helpPopup.div.classList.add('dirty');
elSave.disabled = false;
if (elAuto.checked) debounce(save, AUTOSAVE_DELAY);
}
function initArea(type) {
return initInput(`#ss-${type}`, type, [], {
get: textToList,
set: list => list.join('\n'),
validate(el) {
const val = el.value;
el.rows = val.match(/^/gm).length + !val.endsWith('\n');
},
});
}
function initCheckbox(el, key, defVal) {
return initInput(el, key, Boolean(defVal), {dom: 'checked'});
}
function initInput(el, key, defVal, {
dom = 'value', // DOM property name
get = PASS, // transformer function(val) after getting DOM value
set = PASS, // transformer function(val) before setting DOM value
validate = PASS, // function(el) - return `false` to prevent saving
} = {}) {
if (typeof el === 'string') {
el = $(el, ui);
}
el.oninput = () => {
if (validate(el) !== false) {
autosave(el, {dom, get, key});
}
};
return () => {
let val = style[key];
val = set(val != null ? val : defVal);
// Skipping if unchanged to preserve the Undo history of the input
if (el[dom] !== val) el[dom] = val;
validate(el);
};
}
function initRadio(name, key, defVal) {
$(`#${name}`, ui).oninput = e => {
if (e.target.checked) {
autosave(e.target, {key});
}
};
return () => {
const val = style[key] || defVal;
const el = $(`[name="${name}"][value="${val}"]`, ui);
el.checked = true;
};
}
function save() {
pendingSetters.forEach(saveValue);
pendingSetters.clear();
helpPopup.div.classList.remove('dirty');
elSave.disabled = true;
}
function saveValue({dom = 'value', get = PASS, key}, el) {
return API.styles.config(style.id, key, get(el[dom]));
}
function textToList(text) {
return text.split(/\n/).map(s => s.trim()).filter(Boolean);
}
function update() {
updaters.forEach(fn => fn());
}
}

View File

@ -4,7 +4,7 @@
/* global MozDocMapper */// util.js
/* global MozSectionFinder */
/* global MozSectionWidget */
/* global RX_META debounce */// toolbox.js
/* global URLS debounce sessionStore */// toolbox.js
/* global chromeSync */// storage-util.js
/* global cmFactory */
/* global editor */
@ -14,31 +14,23 @@
'use strict';
/* exported SourceEditor */
async function SourceEditor() {
function SourceEditor() {
const {style, /** @type DirtyReporter */dirty} = editor;
const DEFAULT_TEMPLATE = `
/* ==UserStyle==
@name ${''/* a trick to preserve the trailing spaces */}
@namespace github.com/openstyles/stylus
@version 1.0.0
@description A new userstyle
@author Me
==/UserStyle== */
`.replace(/^\s+/gm, '');
let savedGeneration;
let placeholderName = '';
let prevMode = NaN;
$$remove('.sectioned-only');
$('#header').on('wheel', headerOnScroll);
$('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor'));
$('#save-button').on('split-btn', saveTemplate);
if (!style.id) setupNewStyle(style);
const cm = cmFactory.create($('.single-editor'));
const sectionFinder = MozSectionFinder(cm);
const sectionWidget = MozSectionWidget(cm, sectionFinder);
editor.livePreview.init(preprocess);
if (!style.id) setupNewStyle(await editor.template);
editor.livePreview.init(preprocess, style.id);
createMetaCompiler(meta => {
style.usercssData = meta;
style.name = meta.name;
@ -53,17 +45,9 @@ async function SourceEditor() {
sections: sectionFinder.sections,
replaceStyle,
updateLivePreview,
updateMeta,
closestVisible: () => cm,
getEditors: () => [cm],
getEditorTitle: () => '',
getValue: asObject => asObject
? {
customName: style.customName,
enabled: style.enabled,
sourceCode: cm.getValue(),
}
: cm.getValue(),
getSearchableInputs: () => [],
prevEditor: nextPrevSection.bind(null, -1),
nextEditor: nextPrevSection.bind(null, 1),
@ -72,24 +56,28 @@ async function SourceEditor() {
if (sec) {
sectionFinder.updatePositions(sec);
cm.jumpToPos(sec.start);
cm.focus();
}
},
async saveImpl() {
async save() {
if (!dirty.isDirty()) return;
const sourceCode = cm.getValue();
try {
const {customName, enabled, id} = style;
let res = !id && await API.usercss.build({sourceCode, checkDup: true, metaOnly: true});
if (res && res.dup) {
if (!id &&
(await API.usercss.build({sourceCode, checkDup: true, metaOnly: true})).dup) {
messageBoxProxy.alert(t('usercssAvoidOverwriting'), 'danger', t('genericError'));
} else {
res = await API.usercss.editSave({customName, enabled, id, sourceCode});
// Awaiting inside `try` so that exceptions go to our `catch`
await replaceStyle(res.style);
await replaceStyle(
await API.usercss.editSave({customName, enabled, id, sourceCode}));
}
showLog(res);
} catch (err) {
showSaveError(err);
const i = err.index;
const isNameEmpty = i > 0 &&
err.code === 'missingValue' &&
sourceCode.slice(sourceCode.lastIndexOf('\n', i - 1), i).trim().endsWith('@name');
return isNameEmpty
? saveTemplate(sourceCode)
: showSaveError(err);
}
},
scrollToEditor: () => {},
@ -101,6 +89,7 @@ async function SourceEditor() {
'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
}, {runNow: true});
editor.applyScrollInfo(cm);
cm.clearHistory();
cm.markClean();
savedGeneration = cm.changeGeneration();
@ -120,23 +109,15 @@ async function SourceEditor() {
if (!$isTextInput(document.activeElement)) {
cm.focus();
}
editor.applyScrollInfo(cm); // WARNING! Place it after all cm.XXX calls that change scroll pos
async function preprocess(style) {
const res = await API.usercss.build({
const {style: newStyle} = await API.usercss.build({
styleId: style.id,
sourceCode: style.sourceCode,
assignVars: true,
});
showLog(res);
delete res.style.enabled;
return Object.assign(style, res.style);
}
/** Shows the console.log output from the background worker stored in `log` property */
function showLog(data) {
if (data.log) data.log.forEach(args => console.log(...args));
return data;
delete newStyle.enabled;
return Object.assign(style, newStyle);
}
function updateLivePreview() {
@ -168,21 +149,34 @@ async function SourceEditor() {
return name;
}
function setupNewStyle(tpl) {
const comment = `/* ${t('usercssReplaceTemplateSectionBody')} */`;
const sec0 = style.sections[0];
sec0.code = ' '.repeat(prefs.get('editor.tabSize')) + comment;
if (Object.keys(sec0).length === 1) { // the only key is 'code'
sec0.domains = ['example.com'];
async function setupNewStyle(style) {
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
let section = MozDocMapper.styleToCss(style);
if (!section.includes('@-moz-document')) {
style.sections[0].domains = ['example.com'];
section = MozDocMapper.styleToCss(style);
}
style.name = [style.name, new Date().toLocaleString()].filter(Boolean).join(' - ');
style.sourceCode = (tpl || DEFAULT_TEMPLATE)
.replace(/(@name)(?:([\t\x20]+).*|\n)/, (_, k, space) => `${k}${space || ' '}${style.name}`)
.replace(/\s*@-moz-document[^{]*{([^}]*)}\s*$/g, // stripping dummy sections
(s, body) => body.trim() === comment ? '\n\n' : s)
.trim() +
'\n\n' +
MozDocMapper.styleToCss(style);
const DEFAULT_CODE = `
/* ==UserStyle==
@name ${''/* a trick to preserve the trailing spaces */}
@namespace github.com/openstyles/stylus
@version 1.0.0
@description A new userstyle
@author Me
==/UserStyle== */
`.replace(/^\s+/gm, '');
dirty.clear('sourceGeneration');
style.sourceCode = '';
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate);
code = code || DEFAULT_CODE;
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
`${str}${space ? '' : ' '}${placeholderName}`);
// strip the last dummy section if any, add an empty line followed by the section
style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section;
cm.startOperation();
cm.setValue(style.sourceCode);
cm.clearHistory();
@ -194,60 +188,60 @@ async function SourceEditor() {
function updateMeta() {
const name = style.customName || style.name;
$('#name').value = name;
if (name !== placeholderName) {
$('#name').value = name;
}
$('#enabled').checked = style.enabled;
$('#url').href = style.url;
editor.updateName();
cm.setPreprocessor((style.usercssData || {}).preprocessor);
}
async function replaceStyle(newStyle, draft) {
function replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) {
savedGeneration = cm.changeGeneration();
dirty.clear('sourceGeneration');
editor.useSavedStyle(newStyle);
}
if (codeIsUpdated === false || sameCode) {
updateEnvironment();
dirty.clear('enabled');
updateLivePreview();
return;
}
if (draft || await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
editor.useSavedStyle(newStyle);
Promise.resolve(messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))).then(ok => {
if (!ok) return;
updateEnvironment();
if (!sameCode) {
const si0 = draft && draft.si.cms[0];
const cursor = !si0 && cm.getCursor();
const cursor = cm.getCursor();
cm.setValue(style.sourceCode);
if (si0) {
editor.applyScrollInfo(cm, si0);
} else {
cm.setCursor(cursor);
}
cm.setCursor(cursor);
savedGeneration = cm.changeGeneration();
}
if (sameCode) {
// the code is same but the environment is changed
updateLivePreview();
}
if (!draft) {
dirty.clear();
dirty.clear();
});
function updateEnvironment() {
if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
}
sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle);
$('#preview-label').classList.remove('hidden');
updateMeta();
editor.livePreview.toggle(Boolean(style.id));
}
}
async function saveTemplate() {
const res = await messageBoxProxy.show({
contents: t('usercssReplaceTemplateConfirmation'),
className: 'center',
buttons: [t('confirmYes'), t('confirmNo'), {
textContent: t('genericResetLabel'),
title: t('restoreTemplate'),
}],
});
if (res.enter || res.button !== 1) {
async function saveTemplate(code) {
if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) {
const key = chromeSync.LZ_KEY.usercssTemplate;
const code = res.button === 2 ? DEFAULT_TEMPLATE : cm.getValue();
await chromeSync.setLZValue(key, code);
if (await chromeSync.getLZValue(key) !== code) {
messageBoxProxy.alert(t('syncStorageErrorSaving'));
@ -313,7 +307,7 @@ async function SourceEditor() {
if (_cm !== cm) {
return;
}
const match = text.match(RX_META);
const match = text.match(URLS.rxMETA);
if (!match) {
return [];
}
@ -324,17 +318,13 @@ async function SourceEditor() {
if (errors.every(err => err.code === 'unknownMeta')) {
onUpdated(metadata);
}
cache = errors.map(({code, index, args, message}) => {
const isUnknownMeta = code === 'unknownMeta';
const typo = isUnknownMeta && args[1] ? 'Typo' : ''; // args[1] may be present but undefined
return ({
from: cm.posFromIndex((index || 0) + match.index),
to: cm.posFromIndex((index || 0) + match.index),
message: code && t(`meta_${code}${typo}`, args, false) || message,
severity: isUnknownMeta ? 'warning' : 'error',
rule: code,
});
});
cache = errors.map(err => ({
from: cm.posFromIndex((err.index || 0) + match.index),
to: cm.posFromIndex((err.index || 0) + match.index),
message: err.code && t(`meta_${err.code}`, err.args, false) || err.message,
severity: err.code === 'unknownMeta' ? 'warning' : 'error',
rule: err.code,
}));
meta = match[0];
metaIndex = match.index;
return cache;

View File

@ -1,96 +0,0 @@
/* global $ $create $remove messageBoxProxy showSpinner toggleDataset */// dom.js
/* global API msg */// msg.js
/* global URLS */// toolbox.js
/* global editor */
/* global t */// localization.js
'use strict';
(() => {
//#region Main
const ERROR_TITLE = 'UserStyles.world ' + t('genericError');
const PROGRESS = '#usw-progress';
let spinnerTimer = 0;
let prevCode = '';
msg.onExtension(request => {
if (request.method === 'uswData' &&
request.style.id === editor.style.id) {
Object.assign(editor.style, request.style);
updateUI();
}
});
window.on('domReady', () => {
updateUI();
$('#usw-publish-style').onclick = disableWhileActive(publishStyle);
$('#usw-disconnect').onclick = disableWhileActive(disconnect);
}, {once: true});
async function publishStyle() {
const {id} = editor.style;
if (await API.data.has('usw' + id) &&
!await messageBoxProxy.confirm(t('publishRetry'), 'danger', ERROR_TITLE)) {
return;
}
const code = editor.getValue();
const isDiff = code !== prevCode;
const res = isDiff ? await API.usw.publish(id, code) : t('importReportUnchanged');
const title = `${new Date().toLocaleString()}\n${res}`;
const failed = /^Error:/.test(res);
$(PROGRESS).append(...failed && [
$create('div.error', {title}, res),
$create('div', t('publishReconnect')),
] || [
$create(`span.${isDiff ? 'success' : 'unchanged'}`, {title}),
]);
if (!failed) prevCode = code;
}
async function disconnect() {
await API.usw.revoke(editor.style.id);
prevCode = null; // to allow the next publishStyle to upload style
}
function updateUI(style = editor.style) {
const usw = style._usw || {};
const section = $('#publish');
toggleDataset(section, 'connected', usw.token);
for (const type of ['name', 'description']) {
const el = $(`dd[data-usw="${type}"]`, section);
el.textContent = el.title = usw[type] || '';
}
const elUrl = $('#usw-url');
elUrl.href = `${URLS.usw}${usw.id ? `style/${usw.id}` : ''}`;
elUrl.textContent = t('publishUsw').replace(/<(.+)>/, `$1${usw.id ? `#${usw.id}` : ''}`);
}
//#endregion
//#region Utility
function disableWhileActive(fn) {
/** @this {Element} */
return async function () {
this.disabled = true;
timerOn();
await fn().catch(console.error);
timerOff();
this.disabled = false;
};
}
function timerOn() {
if (!spinnerTimer) {
$(PROGRESS).textContent = '';
spinnerTimer = setTimeout(showSpinner, 250, PROGRESS);
}
}
function timerOff() {
$remove(`${PROGRESS} .lds-spinner`);
clearTimeout(spinnerTimer);
spinnerTimer = 0;
}
//#endregion
})();

View File

@ -7,54 +7,47 @@
const helpPopup = {
/**
* @param {string} title - plain text
* @param {string|Node} body - Node, html or plain text
* @param {Node} [props] - DOM props for the popup element
* @returns {Element} the popup
*/
show(title = '', body, props) {
show(title = '', body) {
const div = $('#help-popup');
const contents = $('.contents', div);
div.style = '';
div.className = '';
contents.textContent = '';
Object.assign(div, props);
if (body) {
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
}
$('.title', div).textContent = title;
$('.dismiss', div).onclick = helpPopup.close;
window.on('keydown', helpPopup.close, true);
div.style.display = 'block';
// reset any inline styles
div.style = 'display: block';
helpPopup.originalFocus = document.activeElement;
helpPopup.div = div;
moveFocus(div, 0);
return div;
},
close(event) {
let el;
const canClose =
!event ||
event.type === 'click' ||
getEventKeyName(event) === 'Escape' && !$('.CodeMirror-hints, #message-box') && (
!(el = document.activeElement) ||
!el.closest('#search-replace-dialog')
event.type === 'click' || (
getEventKeyName(event) === 'Escape' &&
!$('.CodeMirror-hints, #message-box') && (
!document.activeElement ||
!document.activeElement.closest('#search-replace-dialog') &&
document.activeElement.matches(':not(input), .can-close-on-esc')
)
);
const {div} = helpPopup;
const div = $('#help-popup');
if (!canClose || !div) {
return;
}
if (event && (el = div.codebox) && !el.options.readOnly && !el.isClean()) {
if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
setTimeout(async () => {
const ok = await messageBoxProxy.confirm(t('confirmDiscardChanges'));
return ok && helpPopup.close();
});
return;
}
if (div.contains(document.activeElement) && (el = helpPopup.originalFocus)) {
el.focus();
if (div.contains(document.activeElement) && helpPopup.originalFocus) {
helpPopup.originalFocus.focus();
}
const contents = $('.contents', div);
div.style.display = '';
@ -110,74 +103,55 @@ function clipString(str, limit = 100) {
}
/* exported createHotkeyInput */
function createHotkeyInput(prefId, {buttons = true, onDone}) {
const RX_ERR = new RegExp('^(' + [
/Space/,
/(Shift-)?./, // a single character
/(?=.)(Shift-?|Ctrl-?|Control-?|Alt-?|Meta-?)*(Escape|Tab|Page(Up|Down)|Arrow(Up|Down|Left|Right)|Home|End)?/,
].map(r => r.source || r).join('|') + ')$', 'i');
const initialValue = prefs.get(prefId);
const input = $create('input', {
function createHotkeyInput(prefId, onDone = () => {}) {
return $create('input', {
type: 'search',
spellcheck: false,
onpaste: e => onkeydown(e, e.clipboardData.getData('text')),
onkeydown,
});
buttons = buttons && [
['confirmOK', 'Enter'],
['undo', initialValue],
['genericResetLabel', ''],
].map(([label, val]) =>
$create('button', {onclick: e => onkeydown(e, val)}, t(label)));
const [btnOk, btnUndo, btnReset] = buttons || [];
onkeydown(null, initialValue);
return buttons
? $create('fragment', [input, $create('.buttons', buttons)])
: input;
function onkeydown(e, key) {
let newValue;
if (e && e.type === 'keydown') {
key = getEventKeyName(e);
}
switch (e && key) {
case 'Tab':
case 'Shift-Tab':
value: prefs.get(prefId),
onkeydown(event) {
const key = CodeMirror.keyName(event);
if (key === 'Tab' || key === 'Shift-Tab') {
return;
case 'BackSpace':
case 'Delete':
newValue = '';
break;
case 'Enter':
if (input.checkValidity() && onDone) onDone();
break;
case 'Escape':
if (onDone) onDone();
break;
default:
newValue = key.replace(/\b.$/, c => c.toUpperCase());
}
if (newValue != null) {
const error = RX_ERR.test(newValue) ? t('genericError') : '';
if (e && !error) prefs.set(prefId, newValue);
input.setCustomValidity(error);
input.value = newValue;
input.focus();
if (buttons) {
btnOk.disabled = Boolean(error);
btnUndo.disabled = newValue === initialValue;
btnReset.disabled = !newValue;
}
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
}
event.preventDefault();
event.stopPropagation();
switch (key) {
case 'Enter':
if (this.checkValidity()) onDone(true);
return;
case 'Esc':
onDone(false);
return;
default:
// disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys
if (!key || new RegExp('^(' + [
'(Back)?Space',
'(Shift-)?.', // a single character
'(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)',
].join('|') + ')$', 'i').test(key)) {
this.value = key || this.value;
this.setCustomValidity('Not allowed');
return;
}
}
this.value = key;
this.setCustomValidity('');
prefs.set(prefId, key);
},
oninput() {
// fired on pressing "x" to clear the field
prefs.set(prefId, '');
},
onpaste(event) {
event.preventDefault();
},
});
}
/* exported showCodeMirrorPopup */
function showCodeMirrorPopup(title, html, options) {
const popup = helpPopup.show(title, html, {className: 'big'});
const popup = helpPopup.show(title, html);
popup.classList.add('big');
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
mode: 'css',
@ -192,7 +166,7 @@ function showCodeMirrorPopup(title, html, options) {
}, options));
cm.focus();
$.root.style.pointerEvents = 'none';
document.documentElement.style.pointerEvents = 'none';
popup.style.pointerEvents = 'auto';
const onKeyDown = event => {
@ -207,7 +181,7 @@ function showCodeMirrorPopup(title, html, options) {
window.on('closeHelp', () => {
window.off('keydown', onKeyDown, true);
$.root.style.removeProperty('pointer-events');
document.documentElement.style.removeProperty('pointer-events');
cm = popup.codebox = null;
}, {once: true});

View File

@ -1,160 +0,0 @@
@media screen and (prefers-color-scheme: dark), dark {
:root {
/* Comfortable dark themes don't use absolutes so the range is compressed */
--c00: hsl(0, 0%, 80%);
--c10: hsl(0, 0%, 73.5%);
--c20: hsl(0, 0%, 66%);
--c30: hsl(0, 0%, 59.5%);
--c40: hsl(0, 0%, 53%);
--c45: hsl(0, 0%, 49.75%);
--c50: hsl(0, 0%, 46.5%);
--c60: hsl(0, 0%, 40%);
--c65: hsl(0, 0%, 36.75%);
--c70: hsl(0, 0%, 33.5%);
--c75: hsl(0, 0%, 30.25%);
--c80: hsl(0, 0%, 27%);
--c85: hsl(0, 0%, 23.75%);
--c90: hsl(0, 0%, 20.5%);
--c95: hsl(0, 0%, 17.25%);
--c100: hsl(0, 0%, 14%);
/* min/max are exposed in case we want to use an overdrive color for emphasis */
--cmin: hsl(0, 0%, 100%);
--cmax: hsl(0, 0%, 0%);
--accent-1: hsl(180, 100%, 95%);
--accent-3: hsl(180, 30%, 18%);
--input-bg: var(--c95);
--red1: hsl(0, 85%, 55%);
}
textarea,
input[type=url],
input[type=time] {
background-color: var(--input-bg);
color: var(--fg);
}
input::-webkit-inner-spin-button {
filter: invert(.8);
}
input[type=radio]:checked:after {
background-color: var(--fg);
}
input[type=time]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
select {
background-color: var(--bg);
}
.onoffswitch {
--knob: var(--c50);
}
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
background-color: var(--bg) !important;
border: 0;
}
::-webkit-scrollbar {
width: 17px;
height: 17px;
background: var(--bg);
}
::-webkit-scrollbar-corner {
background: var(--bg);
border: 0;
}
/* buttons */
::-webkit-scrollbar-button:single-button {
height: 17px;
width: 17px;
background-size: 9px;
background-position: 4px 7px;
background-repeat: no-repeat;
}
::-webkit-scrollbar-button:horizontal:single-button {
background-position: 7px 4px;
}
/* up */
::-webkit-scrollbar-button:single-button:vertical:decrement {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='1,0 0,1 2,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:decrement:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='1,0 0,1 2,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:decrement:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='1,0 0,1 2,1'/></svg>");
}
/* down */
::-webkit-scrollbar-button:single-button:vertical:increment {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='0,0 2,0 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:increment:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='0,0 2,0 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:increment:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='0,0 2,0 1,1'/></svg>");
}
/* left */
::-webkit-scrollbar-button:single-button:horizontal:decrement {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='0,1 1,2 1,0'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:decrement:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='0,1 1,2 1,0'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:decrement:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='0,1 1,2 1,0'/></svg>");
}
/* right */
::-webkit-scrollbar-button:single-button:horizontal:increment {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='0,0 0,2 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:increment:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='0,0 0,2 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:increment:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='0,0 0,2 1,1'/></svg>");
}
::-webkit-scrollbar-track-piece {
background: hsl(0, 0%, 17%);
border: 1px solid var(--bg);
}
::-webkit-scrollbar-track-piece:hover {
background: hsl(0, 0%, 20%);
}
::-webkit-scrollbar-track-piece:active {
background: hsl(0, 0%, 25%);
}
::-webkit-scrollbar-thumb {
background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1' fill='hsl(0, 0%, 30%)'><rect width='1' height='1'/></svg>") 2px 2px no-repeat;
}
::-webkit-scrollbar-thumb:horizontal {
background-size: 100% 13px;
}
::-webkit-scrollbar-thumb:vertical {
background-size: 13px 100%;
}
::-webkit-scrollbar-thumb:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1' fill='hsl(0, 0%, 33%)'><rect width='1' height='1'/></svg>");
}
::-webkit-scrollbar-thumb:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1' fill='hsl(0, 0%, 40%)'><rect width='1' height='1'/></svg>");
}
::-webkit-resizer {
background: var(--input-bg) linear-gradient(-45deg,
transparent 3px, #888 3px,
#888 4px, transparent 4px,
transparent 6px, #888 6px,
#888 7px, transparent 7px) no-repeat;
border: 2px solid transparent;
}
:-webkit-autofill {
box-shadow: 0 0 0 1000px var(--input-bg) inset;
-webkit-text-fill-color: #fff;
}
@supports (-moz-appearance: none) {
/* Workarounds for FF bugs/quirks */
textarea {
border: 1px solid var(--c65);
}
* {
scrollbar-color: var(--c75) var(--bg);
}
}
}

View File

@ -1,52 +1,15 @@
@supports not (accent-color: red) {
/* This suppresses a bug in all? browsers: they apply transitions during page load.
* It was fixed by crrev.com/886802 in Chrome 93, which we detect via `accent-color`.
* Using an increased specificity to override sane selectors in user styles.
* Using \1 to simplify js code because \0 is converted to \xFFFD per spec. */
html#stylus #header *:not(#\1transition-suppressor) {
transition: none !important;
}
}
:root {
--family: Arial, "Helvetica Neue", Helvetica, system-ui, sans-serif;
--input-height: 22px;
--cmin: hsl(0, 0%, 00%);
--c00: hsl(0, 0%, 00%);
--c10: hsl(0, 0%, 10%);
--c20: hsl(0, 0%, 20%);
--c30: hsl(0, 0%, 30%);
--c40: hsl(0, 0%, 40%);
--c45: hsl(0, 0%, 45%);
--c50: hsl(0, 0%, 50%);
--c60: hsl(0, 0%, 60%);
--c65: hsl(0, 0%, 65%);
--c70: hsl(0, 0%, 70%);
--c75: hsl(0, 0%, 75%);
--c80: hsl(0, 0%, 80%);
--c85: hsl(0, 0%, 85%);
--c90: hsl(0, 0%, 90%);
--c95: hsl(0, 0%, 95%);
--c100: hsl(0, 0%, 100%);
--cmax: hsl(0, 0%, 100%);
--bg: var(--c100);
--fg: var(--c00);
--accent-1: hsl(180, 100%, 15%);
--accent-2: hsl(180, 50%, 40%);
--accent-3: hsl(180, 40%, 69%);
--red1: hsl(0, 70%, 45%);
}
body {
font: normal 12px var(--family);
background-color: var(--bg);
color: var(--fg);
margin: 0;
font: normal 12px Arial, system-ui, sans-serif;
}
body:lang(ja) {
font-family: Arial, 'Meiryo UI', 'MS Gothic', system-ui, sans-serif;
}
body:lang(zh-CN) {
font-family: Arial, 'Microsoft YaHei UI', 'Microsoft YaHei', system-ui, sans-serif;
}
body:lang(zh-TW),
body:lang(zh-HK) {
font-family: Arial, 'Microsoft JhengHei UI', 'Microsoft JhengHei', system-ui, sans-serif;
@ -61,12 +24,11 @@ button {
overflow: hidden;
text-overflow: ellipsis;
padding: 2px 7px;
border: 1px solid var(--c60);
border: 1px solid hsl(0, 0%, 62%);
font: inherit;
font-size: 13px;
line-height: 1.2;
color: var(--fg);
background-color: var(--bg);
color: #000;
background-color: hsl(0, 0%, 100%);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwGBBwIHvKt6QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVQI12NoaGgQZ2JgYGBkYmBgYGZiYGBggrMY4VxsYsyoskQQCB2MWAxAMhkADVECDhlW9CoAAAAASUVORK5CYII=');
background-repeat: repeat-x;
background-size: 100% 100%;
@ -74,79 +36,39 @@ button {
}
button:not(:disabled):hover {
background-color: var(--c95);
border-color: var(--c50);
background-color: hsl(0, 0%, 95%);
border-color: hsl(0, 0%, 52%);
}
button:active {
background-color: var(--c95);
border-color: var(--c50);
background-color: hsl(0, 0%, 95%);
border-color: hsl(0, 0%, 52%);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwJARIWJNZvuQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAMElEQVQI12NoaGgIZmJgYPjLxMDA8I+JgYHhP5z1Dy7xH5X7jxQCzWQ0A9DEILYBABm5HtJk2jPHAAAAAElFTkSuQmCC');
background-repeat: repeat-x;
background-size: 100% 100%;
}
button .svg-icon {
cursor: auto;
}
[data-ui-theme="light"] button .svg-icon {
/* Our svgs are pixel-aligned so the default #000 looks too strong */
fill: #333;
}
/* For some odd reason these hovers appear lighter than all other button hovers in every browser */
#message-box-buttons button:not(:disabled):hover {
background-color: var(--c90);
border-color: var(--c50);
background-color: hsl(0, 0%, 90%);
border-color: hsl(0, 0%, 50%);
}
input {
font: inherit;
border: 1px solid var(--c65);
border: 1px solid hsl(0, 0%, 66%);
transition: border-color .1s, box-shadow .1s;
}
input:not([type]),
input[type=text],
input[type=number],
input[type=search] {
background: var(--bg);
color: var(--fg);
height: var(--input-height);
min-height: var(--input-height)!important;
line-height: var(--input-height);
box-sizing: border-box;
background: #fff;
color: #000;
height: 22px;
min-height: 22px!important;
line-height: 22px;
padding: 0 3px;
border: 1px solid var(--c65);
}
input:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
.svg-icon {
cursor: pointer;
vertical-align: middle;
transition: fill .5s;
width: 20px;
height: 20px;
fill: var(--c40);
}
.svg-icon:hover {
fill: var(--fg);
}
.svg-icon.info {
width: 14px;
height: 16px;
margin-left: .5ex;
}
.svg-icon.config {
width: 16px;
height: 16px;
border: 1px solid hsl(0, 0%, 66%);
}
.svg-icon.checked {
@ -154,7 +76,7 @@ input:invalid {
height: 8px;
width: 8px;
display: none;
fill: var(--fg);
fill: #000;
margin: 2px 0 0 2px;
}
@ -169,7 +91,7 @@ input[type="checkbox"]:not(.slider) {
position: absolute;
left: 0;
top: 0;
border: 1px solid var(--c45);
border: 1px solid hsl(0, 0%, 46%);
height: 12px;
width: 12px;
display: inline-flex;
@ -180,8 +102,8 @@ input[type="checkbox"]:not(.slider) {
}
input[type="checkbox"]:not(.slider):hover {
border-color: var(--c30);
background-color: var(--c80);
border-color: hsl(0, 0%, 32%);
background-color: hsl(0, 0%, 82%);
}
input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
@ -193,33 +115,29 @@ input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
input[type="checkbox"]:not(.slider):disabled {
background-color: transparent;
border-color: var(--c50);
border-color: hsl(0, 0%, 50%);
}
input[type="checkbox"]:not(.slider):disabled + .svg-icon.checked {
fill: var(--c50);
fill: hsl(0, 0%, 50%);
}
input[type="checkbox"]:not(.slider):disabled + .svg-icon.checked + span {
color: var(--c50);
color: hsl(0, 0%, 50%);
}
label {
transition: color .1s;
}
.checkbox-wrapper {
padding-left: 16px;
position: relative;
}
select {
-moz-appearance: none;
-webkit-appearance: none;
height: var(--input-height);
height: 22px;
font: inherit;
color: var(--fg);
color: #000;
background-color: transparent;
border: 1px solid var(--c65);
border: 1px solid hsl(0, 0%, 66%);
padding: 0 20px 0 6px;
transition: color .5s;
}
@ -237,7 +155,7 @@ select {
display: inline-flex;
height: 14px;
width: 14px;
fill: var(--fg);
fill: #000;
position: absolute;
top: 4px;
right: 4px;
@ -247,15 +165,15 @@ select {
input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
background: var(--c90);
background: hsl(0, 0%, 88%);
border-radius: 50%;
border: 1px solid var(--c60);
border: 1px solid hsl(0, 0%, 60%);
cursor: default;
height: 13px;
width: 13px;
position: relative;
margin: 0 4px 1px 0;
}
input[type="radio"]:after {
content: '';
background-color: transparent;
@ -269,15 +187,11 @@ input[type="radio"]:after {
top: 2px;
position: absolute;
}
input[type="radio"]:checked:after {
background-color: var(--c30);
background-color: hsl(0, 0%, 30%);
transform: scale(1);
}
.radio-wrapper {
display: flex;
align-items: center;
line-height: 1.5;
}
/* restore disabled state dimming */
button:disabled,
@ -289,44 +203,13 @@ select[disabled] > option {
select:disabled + .select-arrow,
select[disabled] + .select-arrow {
fill: var(--c50);
}
summary {
-moz-user-select: none;
user-select: none;
fill: hsl(0, 0%, 50%);
}
/* global stuff we use everywhere */
.hidden {
display: none !important;
}
.rel {
position: relative;
}
.abs {
position: absolute;
}
html:not(.all-disabled) body:not(#stylus-popup) #disableAll-label:not([data-persist]) {
display: none;
}
html:not(.all-disabled) #disableAll-label::before {
content: attr(data-on);
}
.all-disabled #disableAll-label::before {
content: attr(data-off);
}
.all-disabled #disableAll-label {
font-weight: bold;
color: var(--red1);
}
.all-disabled #disableAll-label .svg-icon {
fill: var(--red1);
}
.all-disabled #disableAll {
border-color: var(--red1);
}
:focus,
.CodeMirror-focused,
@ -334,7 +217,6 @@ html:not(.all-disabled) #disableAll-label::before {
textarea[data-focused-via-click]:focus,
input:not([type])[data-focused-via-click]:focus, /* same as "text" */
input[type="text"][data-focused-via-click]:focus,
input[type="url"][data-focused-via-click]:focus,
input[type="search"][data-focused-via-click]:focus,
input[type="number"][data-focused-via-click]:focus {
/* Using box-shadow instead of the ugly outline in new Chrome */
@ -348,95 +230,24 @@ input[type="number"][data-focused-via-click]:focus {
box-shadow: none;
}
/* header resizer */
:root {
--header-width: 280px;
--header-resizer-width: 8px;
}
#header-resizer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: var(--header-resizer-width);
box-sizing: border-box;
cursor: e-resize;
border-width: 0 1px;
border-style: solid;
color: hsla(0, 0%, 50%, .5);
border-color: currentColor;
pointer-events: auto;
}
#header-resizer:active {
border-color: var(--c50);
}
#header-resizer::after {
content: '';
position: absolute;
border-right: 2px dotted currentColor;
left: 2px;
width: 0;
height: 100%;
}
body.resizing-h {
cursor: e-resize;
}
body.resizing-v {
cursor: n-resize;
}
body.resizing-h > *,
body.resizing-v > * {
pointer-events: none;
-moz-user-select: none;
user-select: none;
}
/* header resizer - end */
.split-btn {
position: relative;
white-space: nowrap;
--menu-pad: .5em;
}
.split-btn-pedal {
margin-left: -1px !important;
padding-left: .25em !important;
padding-right: .25em !important;
min-width: 0 !important;
}
.split-btn-pedal::after {
--side: 4px;
content: '';
border: var(--side) solid transparent;
display: inline-block;
border-top: calc(var(--side) * 1.3) solid currentColor;
vertical-align: bottom;
}
.split-btn.active .split-btn-pedal {
box-shadow: inset 0 0 100px rgba(0, 0, 0, .2);
}
.split-btn-menu {
background: var(--bg);
position: absolute;
box-shadow: 2px 3px 7px rgba(0, 0, 0, .5);
border: 1px solid hsl(180deg, 50%, 50%);
white-space: nowrap;
cursor: pointer;
padding: .25em 0;
z-index: 1000;
}
.split-btn-menu > * {
padding: var(--menu-pad) 1em;
display: block;
}
.split-btn-menu > :hover {
background-color: hsla(180deg, 50%, 50%, .25);
color: var(--fg);
}
@supports (-moz-appearance: none) {
.moz-appearance-bug .svg-icon.checked,
.moz-appearance-bug .onoffswitch input,
.moz-appearance-bug input[type="radio"]:after {
display: none !important;
}
.moz-appearance-bug input[type="checkbox"] {
-moz-appearance: checkbox !important;
}
.moz-appearance-bug input[type="radio"] {
-moz-appearance: radio !important;
}
.firefox select {
padding: 0 20px 0 2px;
line-height: var(--input-height)!important;
line-height: 22px!important;
}
svg {
@ -446,9 +257,9 @@ body.resizing-v > * {
/* We can customize everything about number inputs except arrows. They're horrible in Linux FF, so we'll hide them unless hovered or focused. */
.firefox.non-windows input[type="number"] {
-moz-appearance: textfield;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--c65);
background: #fff;
color: #000;
border: 1px solid hsl(0, 0%, 66%);
}
.firefox.non-windows input[type="number"]:not(:disabled):hover,
@ -457,14 +268,18 @@ body.resizing-v > * {
}
.firefox.non-windows input[type="color"] {
background: var(--bg);
border: 1px solid var(--c65);
background: #fff;
border: 1px solid hsl(0, 0%, 66%);
padding: 4px;
}
}
@media (max-width: 850px) {
#header-resizer {
display: none !important;
/* Firefox cannot handle fractions in font-size */
.firefox button:not(.install) {
line-height: 13px;
padding: 3px 7px;
}
.firefox.moz-appearance-bug button:not(.install) {
padding: 2px 4px;
}
}

Some files were not shown because too many files have changed in this diff Show More