Resolved to keep search-results.js
This commit is contained in:
derv82 2017-12-02 04:41:10 -08:00
commit 0fcc6c3596
86 changed files with 8211 additions and 2263 deletions

View File

@ -1,2 +1,3 @@
vendor/
vendor-overwrites/
vendor-overwrites/*
!vendor-overwrites/colorpicker

View File

@ -17,6 +17,7 @@ globals:
URLS: false
BG: false
notifyAllTabs: false
sendMessage: false
queryTabs: false
getTab: false
getOwnTab: false

View File

@ -12,8 +12,13 @@ Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExt
## Help
[![Discord][chat-image]][chat-link]
See the [help docs](http://userstyles.org/help/stylish_chrome) or [ask in userstyles.org forum](https://forum.userstyles.org). For Stylus specific questions and suggestions please use [review section](http://add0n.com/stylus.html#reviews) of the FAQs page.
[chat-image]: https://img.shields.io/discord/379521691774353408.svg
[chat-link]: https://discordapp.com/widget?id=379521691774353408
## Contributing
The source is hosted on [GitHub](https://github.com/openstyles/stylus) and pull requests are welcome.

View File

@ -8,7 +8,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "أدخل اسمًا.",
"message": "أدخل اسمًا",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -48,7 +48,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Въведете име.",
"message": "Въведете име",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {

View File

@ -19,6 +19,10 @@
"message": "Styles Exportieren",
"description": ""
},
"manageOnlyUsercss": {
"message": "Nur Usercss styles",
"description": "Checkbox to show only Usercss styles"
},
"optionsUpdateInterval": {
"message": "Automatischer Update- und Installations-Intervall (in Stunden)",
"description": ""
@ -31,6 +35,14 @@
"message": "Exportieren",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "Installieren",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "Ungültige @var Checkbox: Wert muss 0 oder 1 sein",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "Ungültiges JSON Format",
"description": "Setting linter config with invalid JSON"
@ -40,7 +52,7 @@
"description": ""
},
"updateCheckHistory": {
"message": "Verlauf der Aktualisierungs-Überprüfungen",
"message": "Verlauf der Updatesuche",
"description": ""
},
"cm_tabSize": {
@ -52,7 +64,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Bitte einen Namen eingeben.",
"message": "Bitte Namen eingeben",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
@ -68,13 +80,17 @@
"description": "Option to make the style apply to the entered string as a domain"
},
"checkForUpdate": {
"message": "Nach Aktualisierung suchen",
"message": "Nach Update suchen",
"description": "Label for the button to check a single style for an update"
},
"styleNotAppliedRegexpProblemTooltip": {
"message": "Der Style wurde aufgrund ungültiger Regulärer Ausdrücke nicht angewandt.",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"colorpickerSwitchFormatTooltip": {
"message": "Formate wechseln: HEX -> RGB ->HSL.\nShift-Klick, um Richtung umzukehren.\nKürzel: Bild auf- und Bild ab-Tasten.",
"description": "Tooltip for the switch button in the color picker popup in the style editor."
},
"styleRegexpInvalidExplanation": {
"message": "Einige Regeln der Regulären Ausdrücke konnten nicht überprüft werden.",
"description": ""
@ -83,6 +99,10 @@
"message": "Dunkle Browser-Themes",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "Import vom Mozilla Format fehlgeschlagen",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "Zum Style anfügen",
"description": "Label for the button to import a style and append to the existing sections"
@ -92,7 +112,7 @@
"description": ""
},
"updateAllCheckSucceededNoUpdate": {
"message": "Keine Aktualisierung gefunden.",
"message": "Keine Updates gefunden.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"importReportLegendAdded": {
@ -119,6 +139,10 @@
"message": "Ausgegraut",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "Die Version des Styles ist älter als die des bereits installierten.",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "Ja",
"description": "'Yes' button in a confirm dialog"
@ -167,10 +191,18 @@
"message": "Löschen",
"description": ""
},
"confirmDefault": {
"message": "Voreinstellung verwenden",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "Abbrechen",
"description": ""
},
"cm_autoCloseBracketsTooltip": {
"message": "Wenn ( [ { ' \" geschrieben werden, automatisch schließende ) ] } ' \" setzen",
"description": "Label for the checkbox in the style editor."
},
"retrieveBckp": {
"message": "Styles Importieren",
"description": ""
@ -196,7 +228,7 @@
"description": "Option to make the style apply to the entered string as a regular expression"
},
"optionsAdvancedExposeIframesNote": {
"message": "Aktiviert die iFrame-spezifische CSS Auszeichnung wie 'html[stylus-iframe] h1 { display:none; }'",
"message": "Aktiviert die iFrame-spezifische CSS Auszeichnung wie \"html[stylus-iframe] h1 { display:none; }\"",
"description": ""
},
"importReportLegendUpdatedCode": {
@ -228,6 +260,15 @@
"message": "Nur lokale Styles",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"styleMetaErrorPreprocessor": {
"message": "Nicht unterstützter @preprocessor: $preprocessor$",
"description": "Error displayed when the value of @preprocessor is not supported",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"linterIssuesHelp": {
"message": "Folgende Probleme wurden von $link$ gefunden:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
@ -237,18 +278,30 @@
}
}
},
"parseUsercssError": {
"message": "Usercss parsen fehlgeschlagen:",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "Inhalte durchsuchen",
"description": "Label for the search filter textbox on the Manage styles page"
},
"optionsUpdateImportNote": {
"message": "Nach dem Importieren von Styles aus einer alten Version oder von Stylish ist eine einmalige manuelle Überprüfung der Aktualisierungen (Updates) in der Verwaltung nötig. Dies stellt sicher, dass alle Styles auf dem aktuellsten Stand sind.",
"message": "Nach dem Importieren von Styles aus einer alten Version oder von Stylish ist eine einmalige manuelle Updatesuche in der Verwaltung nötig. Dies stellt sicher, dass alle Styles auf dem aktuellsten Stand sind.",
"description": ""
},
"checkAllUpdatesForce": {
"message": "Nochmals überprüfen, ich habe keine Styles bearbeitet!",
"description": "Label for the button to apply all detected updates"
},
"manageOnlyNonUsercss": {
"message": "Keine Usercss styles",
"description": "Checkbox to show only non-Usercss (standard) styles"
},
"liveReloadLabel": {
"message": "Echtzeitaktualisierung",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "Stylus kann nur auf das file:// Protokoll in der URL zugreifen, wenn dies in den Einstellungen der Erweiterung unter chrome://extensions festgelegt wurde.",
"description": "Note in the toolbar popup for file:// URLs"
@ -262,7 +315,7 @@
"description": "Label for the checkbox controlling toolbar badge text."
},
"manageFavicons": {
"message": "Favicons in der 'Gilt für' Spalte anzeigen",
"message": "Favicons in der \"Gilt für\" Spalte anzeigen",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"menuShowBadge": {
@ -313,6 +366,10 @@
"message": "Regulärer Ausdruck ist ungültig.",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "Lizenz",
"description": "Label for the license"
},
"optionsHeading": {
"message": "Optionen",
"description": "Heading for options section on manage page."
@ -335,7 +392,7 @@
"description": "Label for the style maanger opener in the browser action context menu."
},
"styleUpdate": {
"message": "Möchtest Du '$stylename$' wirklich aktualisieren?",
"message": "Möchtest Du \"$stylename$\" wirklich aktualisieren?",
"description": "Confirmation when updating a style",
"placeholders": {
"stylename": {
@ -355,6 +412,10 @@
"message": "Ungültige übersprungen",
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Schreibe neuen Style als usercss",
"description": ""
},
"genericResetLabel": {
"message": "Zurücksetzen",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -384,6 +445,10 @@
"message": "Verwende die /re/ Syntax zur Suche als Regulärer Ausdruck",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "Nützlich für dunkle Themes im neuen Chrome, da dort die Seitenränder nicht mehr gefärbt werden",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "Eine erzwungene Aktualisierung wird die lokalen Änderungen überschreiben.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -396,6 +461,10 @@
"message": "Verwerfe den aktuellen Style-Inhalt und überschreibe ihn mit dem importierten",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "Nach Updates suchen",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "Doppelklick, um Höhe zu maximieren / wiederherzustellen",
"description": "Tooltip for the resize grip in style editor"
@ -421,13 +490,17 @@
"description": "Label for the button to remove an 'applies' entry"
},
"updatesCurrentlyInstalled": {
"message": "Installierte Aktualisierungen:",
"message": "Installierte Updates:",
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
},
"styleToMozillaFormatTitle": {
"message": "Style im Mozilla Format",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "Farbwähler für CSS-Farben",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "Style erstellen für:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -470,11 +543,11 @@
"description": "Label for the button to make a style apply only to specific sites"
},
"installUpdate": {
"message": "Aktualisierung installieren",
"message": "Update installieren",
"description": "Label for the button to install an update for a single style"
},
"optionsCheckUpdate": {
"message": "Alle verfügbaren Aktualisierungen Installieren",
"message": "Alle verfügbaren Updates Installieren",
"description": ""
},
"filteredStyles": {
@ -525,6 +598,10 @@
"message": "Alle Styles deaktivieren",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "Zeige \"Gilt für\" Info",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Dieser Style scheint lokal bearbeitet worden zu sein.",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -546,7 +623,7 @@
"description": "Displayed in style manager when unable to connect to the background page"
},
"checkingForUpdate": {
"message": "Suche nach Aktualisierungen...",
"message": "Suche nach Updates...",
"description": "Text to display when checking a style for an update"
},
"styleRegexpTestFull": {
@ -554,7 +631,7 @@
"description": "RegExp test report: label for the fully matching expressions"
},
"manageMaxTargets": {
"message": "Anzahl der 'Gilt für' Elemente",
"message": "Anzahl der \"Gilt für\" Elemente",
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"popupManageTooltip": {
@ -565,20 +642,43 @@
"message": "Stylus nutzt hierzu den externen Dienst https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "\"$stylename$\" ist bereits installiert. Überschreiben?\nVersion: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "Dieser Style wurde lokal bearbeitet.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
},
"linterRulesLink": {
"message": "Vollständige Liste der Regeln anzeigen",
"message": "Vollständige Liste der Regeln anzeigen für",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "Der Style wurde außerhalb des Editors verändert. Style neu laden?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "Optionen zurücksetzen",
"description": ""
},
"externalUsercssDocument": {
"message": "Dokumentation für Usercss",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "'Löschen' im Editor-Kontextmenü hinzufügen",
"message": "\"Löschen\" im Editor-Kontextmenü hinzufügen",
"description": ""
},
"linterConfigPopupTitle": {
@ -590,6 +690,10 @@
}
}
},
"configureStyle": {
"message": "Konfigurieren",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "Aktualisierte Meta Infos und Codes",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -603,7 +707,7 @@
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
},
"appliesHelp": {
"message": "Lege mit den Einstellungen von 'Gilt für' fest, für welche URLs der Code in diesem Bereich gelten soll.",
"message": "Lege mit den Einstellungen von \"Gilt für\" fest, für welche URLs der Code in diesem Bereich gelten soll.",
"description": "Help text for 'applies to' section"
},
"editStyleHeading": {
@ -618,6 +722,28 @@
"message": "Autovervollständigen bei Eingabe",
"description": "Label for the checkbox in the style editor."
},
"linterCSSLintIncompatible": {
"message": "CSSLint unterstützt $preprocessorname$ nicht",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"styleMetaErrorSelectValueMismatch": {
"message": "Ungültiges @select: Wert existiert nicht in der Liste",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ist kein gültiger Farbcode",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"manageOnlyDisabled": {
"message": "Nur deaktivierte Styles",
"description": "Checkbox to show only disabled styles"
@ -631,21 +757,29 @@
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
},
"updateAllCheckSucceededSomeEdited": {
"message": "Einige Styles wurden nicht überprüft, um dem Verlust von lokalen Bearbeitungen vorzubeugen. Die Aktualisierungen können entweder durch einzelne manuelle Überprüfung oder durch eine erneute Ausführung der Aktualisierung und anschließendem Update für alle Styles erzwungen werden (Lokale Bearbeitungen werden dann überschrieben).",
"message": "Einige Styles wurden nicht überprüft, um dem Verlust von lokalen Bearbeitungen vorzubeugen. Die Aktualisierungen können entweder durch einzelne manuelle Überprüfung oder durch eine erneute Ausführung der Updatesuche (und anschließendem Update) für alle Styles erzwungen werden. Lokale Bearbeitungen werden dann überschrieben.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"stylusUnavailableForURL": {
"message": "Stylus funktioniert nicht auf Seiten wie diesen.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "Weiße Rahmen an den Seiten hinzufügen",
"description": ""
},
"manageOnlyUpdates": {
"message": "Nur mit Aktualisierungen oder Problemen",
"message": "Nur mit Updates oder Problemen",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
},
"addStyleTitle": {
"message": "Style hinzufügen",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "Externer Link",
"description": "Label for external links"
},
"importReplaceLabel": {
"message": "Style überschreiben",
"description": "Label for the button to import and overwrite current style"
@ -662,6 +796,10 @@
"message": "Erweitert",
"description": ""
},
"alphaChannel": {
"message": "Deckkraft",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "Füge den importierten Style an den aktuellen",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -707,25 +845,50 @@
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
},
"checkAllUpdates": {
"message": "Nach Aktualisierungen suchen",
"message": "Nach Updates suchen",
"description": "Label for the button to check all styles for updates"
},
"openOptionsManage": {
"message": "Optionen",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "Farbwähler öffnen",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "Badge auf dem Toolbar-Icon",
"description": ""
},
"installUpdateFrom": {
"message": "Style erhält momentan Updates von $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"message": "Identische übersprungen",
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
},
"manageNewStyleAsUsercss": {
"message": "als Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
},
"optionsPopupWidth": {
"message": "Popup-Breite (in Pixeln)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "Klammern automatisch schließen",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "Neuinstallieren",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "Nicht gespeichert aufgrund folgender ungültiger Einstellungen:",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
@ -734,6 +897,19 @@
"message": "Nein",
"description": "'No' button in a confirm dialog"
},
"styleMissingMeta": {
"message": "Erforderliche Metadaten fehlen: @$key$",
"description": "Error displayed when a mandatory metadata is missing",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"appliesLineWidgetWarning": {
"message": "Funktioniert nicht mit minified CSS",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "Rückgängig",
"description": "Button label"
@ -742,6 +918,14 @@
"message": "Tastaturbelegung",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "zur Supportseite",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "Speichern",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "Neues Verwaltungs Design-Layout.",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -762,14 +946,27 @@
"message": "Ersetzen durch",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"liveReloadError": {
"message": "Bei der Echtzeitaktualisierung der Datei ist ein Fehler aufgetreten",
"description": "The label of live-reload error"
},
"deleteStyleLabel": {
"message": "Löschen",
"description": "Label for the button to delete a style"
},
"updateCheckManualUpdateForce": {
"message": "Aktualisierungen installieren (Lokale Bearbeitungen werden überschrieben)",
"message": "Updates installieren (Lokale Bearbeitungen werden überschrieben)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "Installation des Userstyles fehlgeschlagen!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "Ermögliche Iframes via HTML[stylus-iframe]",
"description": ""
@ -779,9 +976,13 @@
"description": "Label for the button to go to the add style page"
},
"optionsUpdateIntervalNote": {
"message": "Zum Deaktivieren der automatischen Aktualisierungs-Überprüfung den Wert auf 0 setzen.",
"message": "Zum Deaktivieren der automatischen Updateüberprüfung den Wert auf 0 setzen.",
"description": ""
},
"installButtonUpdate": {
"message": "Aktualisieren",
"description": "Label for update button"
},
"backupButtons": {
"message": "Datensicherung",
"description": "Heading for backup"
@ -794,10 +995,22 @@
"message": "Bearbeiten",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "Installiert",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "Autor",
"description": "Label for the style author"
},
"popupOpenEditInWindow": {
"message": "Editor in neuem Fenster öffnen",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "Kann letzten \"Gilt für\" Eintrag nicht entfernen",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "Wähle eine Datei aus oder ziehe die Datei auf diese Seite. (Drag and Drop)",
"description": "Message for backup"
@ -817,5 +1030,9 @@
"description": {
"message": "Gestalte das Web neu mit Stylus, dem Style Manager. Stylus ermöglicht dir ganz einfach Themes und Designs für viele populäre Websites zu installieren.",
"description": "Extension description"
},
"confirmClose": {
"message": "Schließen",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -12,7 +12,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Εισάγετε ένα όνομα.",
"message": "Εισάγετε ένα όνομα",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -127,6 +127,10 @@
"message": "Autocomplete on typing",
"description": "Label for the checkbox in the style editor."
},
"cm_colorpicker": {
"message": "Colorpickers for CSS colors",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"cm_indentWithTabs": {
"message": "Use tabs with smart indentation",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
@ -171,6 +175,14 @@
"message": "Theme",
"description": "Label for the style editor's CSS theme."
},
"colorpickerSwitchFormatTooltip": {
"message": "Switch formats: HEX -> RGB -> HSL.\nShift-click to reverse the direction.\nAlso via PgUp (PageUp), PgDn (PageDown) keys.",
"description": "Tooltip for the switch button in the color picker popup in the style editor."
},
"colorpickerTooltip": {
"message": "Open color picker",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"dysfunctional": {
"message": "Stylus cannot function in private windows because Firefox disallows direct connection to the internal background page context of the extension.",
"description": "Displayed in Firefox when its settings make Stylus dysfunctional"
@ -203,6 +215,10 @@
"message": "Use default",
"description": "'Set to default' button in a confirm dialog"
},
"confirmDiscardChanges": {
"message": "Discard the changes?",
"description": "Generic label or title displayed when trying to close something (not a style) with unsaved changes"
},
"confirmSave": {
"message": "Save",
"description": "'Save' button in a confirm dialog"
@ -422,19 +438,19 @@
"description": "Label for the button to import and overwrite current style"
},
"installButton": {
"message": "Install",
"message": "Install style",
"description": "Label for install button"
},
"installButtonInstalled": {
"message": "Installed",
"message": "Style installed",
"description": "Text displayed when the style is successfully installed"
},
"installButtonUpdate": {
"message": "Update",
"message": "Update style",
"description": "Label for update button"
},
"installButtonReinstall": {
"message": "Reinstall",
"message": "Reinstall style",
"description": "Label for reinstall button"
},
"installUpdate": {
@ -529,6 +545,10 @@
"message": "Installed Styles",
"description": "Heading for the manage page"
},
"manageNewStyleAsUsercss": {
"message": "as Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
},
"manageOnlyEnabled": {
"message": "Only enabled styles",
"description": "Checkbox to show only enabled styles"
@ -549,6 +569,14 @@
"message": "(the styles not installed through a userstyles.org page)",
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
},
"manageOnlyNonUsercss": {
"message": "Only non-Usercss styles",
"description": "Checkbox to show only non-Usercss (standard) styles"
},
"manageOnlyUsercss": {
"message": "Only Usercss styles",
"description": "Checkbox to show only Usercss styles"
},
"manageOnlyUpdates": {
"message": "Only with updates or issues",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -623,6 +651,14 @@
"message": "Shift-click or right-click opens manager with styles applicable for current site",
"description": "Tooltip for the 'Manage' button in the popup."
},
"popupHotkeysInfo": {
"message": "<1>-<9>, <0>, also on numpad - toggles Nth style (0 is 10)\n<A>-<Z> toggles first style with a name that starts with the letter\n<Shift> opens editor instead of toggling\n<Numpad +> enables listed styles\n<Numpad > disables listed styles\n<Numpad *> and <`> (backtick) - toggles initially enabled styles; doesn't apply to subsequently enabled styles while the popup is open so you can restore the initial selection after testing stuff: simply disable all, then toggle i.e. <Numpad > <Numpad *>\nMore info on wiki",
"description": "NOTE1: preserve < and > symbols so that <hotkey> is styled as a key.\nNOTE2: the last line is displayed as a text of the link to the wiki page.\nNOTE3: this is the list of hotkeys displayed after clicking the right edge of the extension popup."
},
"popupHotkeysTooltip": {
"message": "Click to see available hotkeys",
"description": "Tooltip displayed when hovering the right edge of the extension popup"
},
"popupOpenEditInWindow": {
"message": "Open editor in a new window",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
@ -825,7 +861,7 @@
}
},
"styleMissingName": {
"message": "Enter a name.",
"message": "Enter a name",
"description": "Error displayed when user saves without providing a name"
},
"styleSaveLabel": {
@ -877,6 +913,10 @@
"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"
},
"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"
},
"toggleStyle": {
"message": "Toggle style",
"description": "Label for the checkbox to enable/disable a style"
@ -946,6 +986,20 @@
"message": "Updates installed:",
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
},
"usercssEditorNamePlaceholder": {
"message": "Specify @name in the code",
"description": "Placeholder text for the empty name input field when creating a new Usercss style"
},
"usercssReplaceTemplateName": {
"message": "Empty @name replaces the default template",
"description": "The text shown after @name when creating a new Usercss style"
},
"usercssReplaceTemplateConfirmation": {
"message": "Replace the default template for new Usercss styles with the current code?"
},
"usercssConfigIncomplete": {
"message": "The style was updated or deleted after the configuration dialog was shown. These variables were not saved to avoid corrupting the style's metadata:"
},
"versionInvalidOlder": {
"message": "The version is older than the installed style.",
"description": "Displayed when the version of style is older than the installed one"

View File

@ -19,6 +19,10 @@
"message": "Exportar estilos",
"description": ""
},
"manageOnlyUsercss": {
"message": "Sólo estilos Usercss",
"description": "Checkbox to show only Usercss styles"
},
"optionsUpdateInterval": {
"message": "Buscar e instalar automáticamente todas las actualizaciones disponibles de estilos de usuario (en horas)",
"description": ""
@ -31,6 +35,14 @@
"message": "Exportar",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "Instalar",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "Casilla @var no válida: El valor debe estar ser 0 o 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "Formato JSON no válido",
"description": "Setting linter config with invalid JSON"
@ -75,6 +87,10 @@
"message": "El estilo no se aplicó debido a su uso incorrecto de 'regexp()'",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"colorpickerSwitchFormatTooltip": {
"message": "Alternar formatos: HEX -> RGB -> HSL.\nMayús+Clic para invertir la dirección.\nTambién con las teclas RePág (Retroceso de página), AvPág (Avance de página).",
"description": "Tooltip for the switch button in the color picker popup in the style editor."
},
"styleRegexpInvalidExplanation": {
"message": "Algunas reglas «regexp()» que no se pudieron compilar en absoluto.",
"description": ""
@ -83,6 +99,10 @@
"message": "Temas de navegador oscuros",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "No se pudo importar desde el formato Mozilla",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "Agregar al estilo",
"description": "Label for the button to import a style and append to the existing sections"
@ -123,6 +143,10 @@
"message": "Atenuado",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "Esta versión es posterior al estilo instalado.",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "Sí",
"description": "'Yes' button in a confirm dialog"
@ -175,10 +199,18 @@
"message": "Eliminar",
"description": ""
},
"confirmDefault": {
"message": "Usar predeterminado",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "Cancelar",
"description": ""
},
"cm_autoCloseBracketsTooltip": {
"message": "Añade automáticamente un equivalente de cierre al escribir uno de apertura de ()[]{}''\"\"",
"description": "Label for the checkbox in the style editor."
},
"retrieveBckp": {
"message": "Importar estilos",
"description": ""
@ -236,6 +268,15 @@
"message": "Sólo estilos creados localmente",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"styleMetaErrorPreprocessor": {
"message": "@preprocessor no soportado: $preprocessor$",
"description": "Error displayed when the value of @preprocessor is not supported",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"linterIssuesHelp": {
"message": "Estos problemas fueron encontrados por $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
@ -245,6 +286,10 @@
}
}
},
"parseUsercssError": {
"message": "Stylus no pudo interpretar usercss:",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "Buscar contenidos",
"description": "Label for the search filter textbox on the Manage styles page"
@ -257,6 +302,14 @@
"message": "Comprobar de nuevo, ¡no he editado ningún estilo!",
"description": "Label for the button to apply all detected updates"
},
"manageOnlyNonUsercss": {
"message": "Sólo estilos no-Usercss",
"description": "Checkbox to show only non-Usercss (standard) styles"
},
"liveReloadLabel": {
"message": "Recargar sobre la marcha",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "Stylus puede acceder a URL file:// solo si activa la casilla correspondiente para la extensión Stylus en la página chrome://extensions.",
"description": "Note in the toolbar popup for file:// URLs"
@ -329,6 +382,10 @@
"message": "La expresión regular proporcionada no es válida.",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "Licencia",
"description": "Label for the license"
},
"optionsHeading": {
"message": "Opciones",
"description": "Heading for options section on manage page."
@ -371,6 +428,10 @@
"message": "no válidos omitidos",
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Escribir nuevo estilo como usercss",
"description": ""
},
"genericResetLabel": {
"message": "Restablecer",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -400,6 +461,10 @@
"message": "Use la sintaxis /re/ para búsquedas con expresiones regulares",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "Útil para temas oscuros en el nuevo Chrome pues ya no pinta los bordes laterales",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "Forzar una actualización sobrescribirá cualquier edición local.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -412,6 +477,10 @@
"message": "Descarta contenidos del estilo actual y los sobrescribe con el estilo importado",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "Buscar actualizaciones",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "Doble-clic para maximizar/restaurar la altura",
"description": "Tooltip for the resize grip in style editor"
@ -444,6 +513,10 @@
"message": "Estilo en formato Mozilla",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "Selectores de color para colores CSS",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "Escribir estilo para:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -545,6 +618,10 @@
"message": "Desactivar todos los estilos",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "Muestra la información 'Se aplica a'",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Este estilo podría haberse editado localmente.",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -585,6 +662,21 @@
"message": "Stylus usa un servicio externo https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "'$stylename$' ya está instalado ¿Sobrescribirlo?\nVersión: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "Este estilo se editó localmente.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -593,6 +685,10 @@
"message": "Vea una lista completa de reglas",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "El estilo se cambió fuera del editor. ¿Desea recagar el estilo?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "Restablecer opciones",
"description": ""
@ -601,6 +697,10 @@
"message": "Código",
"description": "Label for the code for a section"
},
"externalUsercssDocument": {
"message": "Documentación para Usercss",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "Añadir 'Eliminar' al menú contextual del editor",
"description": ""
@ -614,6 +714,10 @@
}
}
},
"configureStyle": {
"message": "Configurar",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "actualizarón tanto meta información como código",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -642,6 +746,32 @@
"message": "Autocompletar al escribir",
"description": "Label for the checkbox in the style editor."
},
"linterCSSLintIncompatible": {
"message": "CSSLint no soporta el 'preprocessor' $preprocessorname$",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"styleMetaErrorSelectValueMismatch": {
"message": "@select no válido: El valor no existe en la lista",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ no es un color válido",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"externalFeedback": {
"message": "Comentarios",
"description": "Label for the external link to send feedback for the style"
},
"manageOnlyDisabled": {
"message": "Sólo estilos deshabilitados",
"description": "Checkbox to show only disabled styles"
@ -662,6 +792,10 @@
"message": "Stylus no funciona en páginas como esta.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "Añadir bordes blancos en los laterales",
"description": ""
},
"manageOnlyUpdates": {
"message": "Sólo con actualizaciones o problemas",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -670,6 +804,14 @@
"message": "Añadir estilo",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "Enlace externo",
"description": "Label for external links"
},
"externalHomepage": {
"message": "Página principal",
"description": "Label for the external link to style's homepage"
},
"importReplaceLabel": {
"message": "Sobrescribir estilo",
"description": "Label for the button to import and overwrite current style"
@ -686,6 +828,10 @@
"message": "Avanzadas",
"description": ""
},
"alphaChannel": {
"message": "Opacidad",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "Agrega el estilo importado al estilo actual",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -738,22 +884,60 @@
"message": "Interfaz de opciones",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "Abrir selector de color",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "Distintivo en el icono de barra de herramientas",
"description": ""
},
"installUpdateFrom": {
"message": "Actualmente el estilo se actualiza desde $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"message": "idénticos omitidos",
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
},
"manageNewStyleAsUsercss": {
"message": "como Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
},
"optionsPopupWidth": {
"message": "Anchura del diálogo emergente (en píxeles)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "Cerrar automáticamente corchetes y comillas",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "Reinstalar",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "No se guardó debido a estos ajustes de configuración no válidos:",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
},
"styleMissingMeta": {
"message": "Metadatos @$key$ ausentes",
"description": "Error displayed when a mandatory metadata is missing",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"appliesLineWidgetWarning": {
"message": "No funciona con CSS minificado",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "Deshacer",
"description": "Button label"
@ -762,6 +946,14 @@
"message": "Mapa de teclado",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "Asistencia",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "Guardar",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "Nuevo diseño de interfaz de gestión",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -782,6 +974,10 @@
"message": "Reemplazar con",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"liveReloadError": {
"message": "Ocurrió un error al vigilar el fichero",
"description": "The label of live-reload error"
},
"deleteStyleLabel": {
"message": "Eliminar",
"description": "Label for the button to delete a style"
@ -790,6 +986,15 @@
"message": "Instalar actualización (se sobrescribirán las ediciones locales)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "¡No se pudo instalar el estilo de usuario!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "Exponer marcos integrados (iframes) vía HTML[stylus-iframe]",
"description": ""
@ -802,6 +1007,10 @@
"message": "Para desactivar las búsquedas automáticas de actualizaciones, establezca el intervalo a 0",
"description": ""
},
"installButtonUpdate": {
"message": "Actualizar",
"description": "Label for update button"
},
"backupButtons": {
"message": "Copia de seguridad",
"description": "Heading for backup"
@ -814,6 +1023,14 @@
"message": "Editar",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "Instalado",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "Autor",
"description": "Label for the style author"
},
"cm_theme": {
"message": "Temas",
"description": "Label for the style editor's CSS theme."
@ -822,6 +1039,10 @@
"message": "Abrir editor en una nueva ventana",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "No se puede eliminar la última entrada 'se aplica a'",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "Seleccione un fichero o arrástrelo y suéltelo en esta página.",
"description": "Message for backup"
@ -841,5 +1062,9 @@
"description": {
"message": "Rediseñe la web con Stylus, un administrador de estilos de usuario. Stylus le permite instalar fácilmente temas y coberturas para muchos sitios populares.",
"description": "Extension description"
},
"confirmClose": {
"message": "Cerrar",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -31,6 +31,14 @@
"message": "Ekspordi",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "Installi",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "Vigane @var märkeruut: väärtus peab olema 0 või 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "Vigane JSON-formaat",
"description": "Setting linter config with invalid JSON"
@ -52,7 +60,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Sisesta nimi.",
"message": "Sisesta nimi",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
@ -83,6 +91,10 @@
"message": "Tumedad brauseriteemad",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "Mozilla-vormingust importimine ebaõnnestus",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "Lisa stiilile",
"description": "Label for the button to import a style and append to the existing sections"
@ -119,6 +131,10 @@
"message": "Tee halliks",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "Versioon on installitud stiilist vanem.",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "Jah",
"description": "'Yes' button in a confirm dialog"
@ -171,6 +187,10 @@
"message": "Kustuta",
"description": ""
},
"confirmDefault": {
"message": "Kasuta vaikesätet",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "Tühista",
"description": ""
@ -241,6 +261,10 @@
}
}
},
"parseUsercssError": {
"message": "Stylus ei suutnud usercss-i analüüsida:",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "Otsi sisu",
"description": "Label for the search filter textbox on the Manage styles page"
@ -253,6 +277,10 @@
"message": "Kontrolli uuesti, ma ei muutnud ühtegi stiili!",
"description": "Label for the button to apply all detected updates"
},
"liveReloadLabel": {
"message": "Reaalajas uuestilaadimine",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "Stylus saab ligi pääseda file:// URLidele ainult siis, kui märgistad vastava kasti Stylus laiendusel chrome://extensions lehel",
"description": "Note in the toolbar popup for file:// URLs"
@ -325,6 +353,10 @@
"message": "Regulaaravaldis on sobimatu.",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "Litsents",
"description": "Label for the license"
},
"optionsHeading": {
"message": "Valikud",
"description": "Heading for options section on manage page."
@ -367,6 +399,10 @@
"message": "sobimatut vahele jäetud",
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Kirjuta uus stiil usercss-ina",
"description": ""
},
"genericResetLabel": {
"message": "Lähtesta",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -396,6 +432,10 @@
"message": "Kasuta /re/ süntaksit regulaaravaldise otsinguks",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "Kasulik tumedate teemade puhul Chrome'is, kuna see ei joonista enam külje piirjooni",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "Uuenduse sundimine kirjutab üle mistahes kohalikud muutused.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -408,6 +448,10 @@
"message": "Tühista praeguse stiili sisu ja kirjuta see üle imporditud stiiliga",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "Kontrolli uuendusi",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "Topeltklõpsa, et maksimeerida/taastada kõrgus",
"description": "Tooltip for the resize grip in style editor"
@ -440,6 +484,10 @@
"message": "Stiil Mozilla-vormingus",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "CSS-värvide värvivalijad",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "Kirjuta stiil:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -541,6 +589,10 @@
"message": "Lülita kõik stiilid välja",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "Näita 'Rakendub' teavet",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Seda stiili võib olla kohalikult muudetud.",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -581,6 +633,21 @@
"message": "Stylus kasutab välist teenust https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "'$stylename$' on juba installitud. Kas kirjutada üle?\nVersioon: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "Seda stiili muudeti kohalikult.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -589,6 +656,10 @@
"message": "Vaata reeglite täielikku loendit",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "Stiili on redaktoriväliselt muudetud. Kas soovid stiili uuesti laadida?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "Lähtesta valikud",
"description": ""
@ -597,6 +668,10 @@
"message": "Kood",
"description": "Label for the code for a section"
},
"externalUsercssDocument": {
"message": "Usercss dokumentatsioon",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "Lisa \"Kustuta\" redaktori kontekstmenüüsse",
"description": ""
@ -610,6 +685,10 @@
}
}
},
"configureStyle": {
"message": "Seadista",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "said uuenduse nii metaandmetele kui koodile",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -638,6 +717,23 @@
"message": "Automaattäide kirjutamisel",
"description": "Label for the checkbox in the style editor."
},
"styleMetaErrorSelectValueMismatch": {
"message": "Vigane @select: väärtust pole loendis olemas",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ ei ole sobiv värv",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"externalFeedback": {
"message": "Tagasiside",
"description": "Label for the external link to send feedback for the style"
},
"manageOnlyDisabled": {
"message": "Ainult keelatud stiilid",
"description": "Checkbox to show only disabled styles"
@ -658,6 +754,10 @@
"message": "Stylus ei tööta sellistel lehtedel.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "Lisa äärtesse valged piirjooned",
"description": ""
},
"manageOnlyUpdates": {
"message": "Ainult uuenduste ja vigadega",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -666,6 +766,14 @@
"message": "Lisa stiil",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "Väline link",
"description": "Label for external links"
},
"externalHomepage": {
"message": "Koduleht",
"description": "Label for the external link to style's homepage"
},
"importReplaceLabel": {
"message": "Kirjuta stiil üle",
"description": "Label for the button to import and overwrite current style"
@ -682,6 +790,10 @@
"message": "Täpsem",
"description": ""
},
"alphaChannel": {
"message": "Läbipaistmatus",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "Lisa imporditud stiil praegusele stiilile",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -734,10 +846,23 @@
"message": "Valikute liides",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "Ava värvivalija",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "Number tööriistaribaikoonil",
"description": ""
},
"installUpdateFrom": {
"message": "Praegu uuendatakse stiili aadressilt $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"message": "identset vahele jäetud",
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
@ -746,6 +871,14 @@
"message": "Hüpikakna laius (pikslites)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "Automaatselt sulge sulud ja jutumärgid",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "Installi uuesti",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "Ei salvestatud nende vigaste seadistuste tõttu:",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
@ -754,6 +887,10 @@
"message": "Ei",
"description": "'No' button in a confirm dialog"
},
"appliesLineWidgetWarning": {
"message": "Ei tööta minimeeritud CSS-iga",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "Võta tagasi",
"description": "Button label"
@ -762,6 +899,14 @@
"message": "Klahvimäärangud",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "Kasutajatugi",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "Salvesta",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "Uus haldusliidese välimus",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -790,6 +935,15 @@
"message": "Installi uuendus (kohalikud muutused kirjutatakse üle)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "Kasutajastiili installimine ebaõnnestus!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "Paljasta iframe-id HTML [stylus-iframe] kaudu",
"description": ""
@ -802,6 +956,10 @@
"message": "Et keelata automaatsed kasutajastiilide uuenduste kontrollid, sea intervalliks 0",
"description": ""
},
"installButtonUpdate": {
"message": "Uuenda",
"description": "Label for update button"
},
"backupButtons": {
"message": "Varunda",
"description": "Heading for backup"
@ -814,6 +972,14 @@
"message": "Muuda",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "Installitud",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "Autor",
"description": "Label for the style author"
},
"cm_theme": {
"message": "Teema",
"description": "Label for the style editor's CSS theme."
@ -822,6 +988,10 @@
"message": "Ava redaktor uues aknas",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "Viimast 'rakendub'-sisestust ei saa eemaldada",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "Vali fail või lohista see siia lehele.",
"description": "Message for backup"
@ -841,5 +1011,9 @@
"description": {
"message": "Disaini veeb ümber kasutajastiilide halduri Stylus'iga. Stylus võimaldab sul lihtsalt installida teemasid ja välimusi mitmetele populaarsetele saitidele.",
"description": "Extension description"
},
"confirmClose": {
"message": "Sulge",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -8,7 +8,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Syötä nimi.",
"message": "Syötä nimi",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -20,7 +20,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Veuillez saisir un nom.",
"message": "Veuillez saisir un nom",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -7,6 +7,10 @@
"message": "alapértelmezett",
"description": "Default CodeMirror CSS theme option on the edit style page"
},
"styleRegexpTestTitle": {
"message": "Illeszkedő megnyitott fülek megjelenítése (kattints az URL-re az arra a fülre való ugráshoz)",
"description": "RegExp test report: title of the report"
},
"bckpInstStyles": {
"message": "Stílusok exportálása",
"description": ""
@ -15,6 +19,10 @@
"message": "Az összes stílus elérhető frissítéseinek automatikus ellenőrzése és telepítése (órában)",
"description": ""
},
"styleEnabledToggleHint": {
"message": "Nyomj Alt-Entert az állapot engedélyezéséhez/letiltásához és a stílus mentéséhez",
"description": "Help text for the '[x] enable' checkbox in the editor"
},
"exportLabel": {
"message": "Exportálás",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -23,6 +31,10 @@
"message": "Háttérszín",
"description": ""
},
"updateCheckHistory": {
"message": "Frissítések ellenőrzésének előzményei",
"description": ""
},
"cm_tabSize": {
"message": "Tabulátorméret",
"description": "Label for the text box controlling tab size option for the style editor."
@ -35,6 +47,14 @@
"message": "Írj be egy nevet!",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
"message": "Előzmények",
"description": "Used in various places to show a history log of something"
},
"shortcutsNote": {
"message": "Gyorsbillentyűk megadása",
"description": ""
},
"appliesDomainOption": {
"message": "URL-ek a doménon",
"description": "Option to make the style apply to the entered string as a domain"
@ -43,6 +63,18 @@
"message": "Frissítések ellenőrzése",
"description": "Label for the button to check a single style for an update"
},
"styleNotAppliedRegexpProblemTooltip": {
"message": "A stílus nem lett alkalmazva a „regexp()” helytelen használata miatt",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"styleRegexpInvalidExplanation": {
"message": "Egyes „regexp()” szabályokat nem lehetett lefordítani.",
"description": ""
},
"optionsIconDark": {
"message": "Sötét böngészőtémák",
"description": ""
},
"importAppendLabel": {
"message": "Hozzáadás stílushoz",
"description": "Label for the button to import a style and append to the existing sections"
@ -55,10 +87,18 @@
"message": "Nem találhatók frissítések.",
"description": "Text that displays when an update all check completed and no updates are available"
},
"importReportLegendAdded": {
"message": "hozzáadva",
"description": "Text after the number of styles added in the report shown after importing styles"
},
"styleFromMozillaFormatPrompt": {
"message": "Mozilla formátumú kód beillesztése",
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
},
"dragDropMessage": {
"message": "Ejtsd a biztonsági másolat fájlt bárhova erre az oldalra az importáláshoz!",
"description": "Drag'n'drop message"
},
"helpAlt": {
"message": "Segítség",
"description": "Alternate text for help buttons"
@ -67,6 +107,10 @@
"message": "Keresés",
"description": "Label before the search input field in the editor shown on Ctrl-F"
},
"manageFaviconsGray": {
"message": "Szürke mód",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"confirmYes": {
"message": "Igen",
"description": "'Yes' button in a confirm dialog"
@ -83,6 +127,10 @@
"message": "Csinosít",
"description": "Label for the CSS-beautifier button on the edit style page"
},
"styleRegexpProblemTooltip": {
"message": "A nem alkalmazott szeckiók száma helytelen 'regexp()' használat miatt",
"description": "Tooltip in the popup for styles that were applied only partially"
},
"styleEnabledLabel": {
"message": "Engedélyezve",
"description": "Label for the enabled state of styles"
@ -95,10 +143,18 @@
"message": "Egy újabb szekció hozzáadása",
"description": "Label for the button to add a section"
},
"styleRegexpTestPartial": {
"message": "Nincs teljes egyezés, így ki vagy hagyva",
"description": "RegExp test report: label for the partially matching expressions"
},
"styleSaveLabel": {
"message": "Mentés",
"description": "Label for save button for style editing"
},
"confirmDelete": {
"message": "Törlés",
"description": ""
},
"confirmCancel": {
"message": "Mégsem",
"description": ""
@ -127,6 +183,14 @@
"message": "Reguláris kifejezésekre (regexp) illeszkedő URL-ek",
"description": "Option to make the style apply to the entered string as a regular expression"
},
"optionsAdvancedExposeIframesNote": {
"message": "Engedélyezi az olyan iframe-specifikus CSS írását, mint a „html[stylus-iframe] h1 { display:none }”",
"description": ""
},
"importReportLegendUpdatedCode": {
"message": "frissített kód",
"description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles"
},
"styleInstall": {
"message": "Telepíted a(z) „$stylename$” nevű stílust a Stylusba?",
"description": "Confirmation when installing a style",
@ -148,10 +212,26 @@
"message": "<a href='https://userstyles.org'>Szerezz be stílusokat a userstyles.org-on</a> | <a href='http://add0n.com/stylus.html'>Kérj segítséget</a>",
"description": "Help text on the manage page"
},
"manageOnlyLocal": {
"message": "Csak helyileg létrehozott stílusok",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"searchStyles": {
"message": "Tartalom keresése",
"description": "Label for the search filter textbox on the Manage styles page"
},
"optionsUpdateImportNote": {
"message": "Amikor régebbi verzióból vagy a Stylishból importálsz stílusokat, egyszer manuálisan frissítsd a stílusokat a stíluskezelőben, hogy megbizonyosodj afelől, hogy mindegyik frissítve van!",
"description": ""
},
"checkAllUpdatesForce": {
"message": "Ellenőrizd újra, nem módosítottam egy stílust sem!",
"description": "Label for the button to apply all detected updates"
},
"unreachableFileHint": {
"message": "A Stylus csak akkor képes hozzáférni a file:// URL-ekhez, ha engedélyezed az erre vonatkozó beállítást a Stylus kiegészítőre a chrome://extensions oldalon.",
"description": "Note in the toolbar popup for file:// URLs"
},
"disableStyleLabel": {
"message": "Letiltás",
"description": "Label for the button to disable a style"
@ -160,6 +240,10 @@
"message": "A jelenlegi oldalon aktív stílusok száma",
"description": "Label for the checkbox controlling toolbar badge text."
},
"manageFavicons": {
"message": "Faviconok az alkalmazási oszlopban",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"menuShowBadge": {
"message": "Aktív stílusok számlálójának mutatása",
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
@ -180,6 +264,10 @@
"message": "Importálás",
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
},
"shortcuts": {
"message": "Gyorsbillentyűk",
"description": "Go to shortcut configuration"
},
"updateCheckFailServerUnreachable": {
"message": "Sikertelen frissítés: nem érhető el a szerver.",
"description": "Text that displays when an update check failed because the update server is unreachable"
@ -192,10 +280,22 @@
"message": "Minden módosítás alkalmazása",
"description": "Label for the button to apply all detected updates"
},
"optionsReset": {
"message": "Beállítások visszaállítása alapértelmezett értékekre.",
"description": ""
},
"optionsCustomizeUpdate": {
"message": "Frissítések",
"description": ""
},
"deleteStyleConfirm": {
"message": "Biztos, hogy törölni akarod ezt a stílust?",
"description": "Confirmation before deleting a style"
},
"optionsCustomizePopup": {
"message": "Felugró",
"description": ""
},
"styleBadRegexp": {
"message": "Érvénytelen regexp.",
"description": "Validation message for a bad regexp in a style"
@ -213,6 +313,10 @@
}
}
},
"optionsIconLight": {
"message": "Világos böngészőtémák",
"description": ""
},
"openStylesManager": {
"message": "A frissítéskezelő megnyitása",
"description": "Label for the style maanger opener in the browser action context menu."
@ -230,6 +334,10 @@
"message": "Szekciók",
"description": "Title for the style sections section"
},
"importReportLegendInvalid": {
"message": "kihagyott érvénytelen",
"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"
},
"editStyleTitle": {
"message": "A(z) $stylename$ stílus szerkesztése",
"description": "Title of the page for editing styles",
@ -247,14 +355,30 @@
"message": "URL-ek adott kezdettel",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"cm_matchHighlightToken": {
"message": "Kurzor alatti kifejezés",
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of of the word/token under cursor even if nothing is selected"
},
"searchRegexp": {
"message": "Használd a /re/ szintaxist a regexp kereséshez",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"updateCheckManualUpdateHint": {
"message": "Egy frissítés erőltetése minden helyi szerkesztést felül fog írni.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"toggleStyle": {
"message": "Stílus be-/kikapcsolása",
"description": "Label for the checkbox to enable/disable a style"
},
"importReplaceTooltip": {
"message": "A jelenlegi stílus tartalmának elvetése és annak felülírása az importált stílussal",
"description": "Label for the button to import and overwrite current style"
},
"cm_resizeGripHint": {
"message": "Kattints duplán a magasság maximalizálásához/visszaállításához",
"description": "Tooltip for the resize grip in style editor"
},
"popupStylesFirst": {
"message": "Parancsok előtti stílusok",
"description": "Label for the checkbox controlling section order in the popup."
@ -275,6 +399,10 @@
"message": "Eltávolítás",
"description": "Label for the button to remove an 'applies' entry"
},
"updatesCurrentlyInstalled": {
"message": "Frissítve:",
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
},
"styleToMozillaFormatTitle": {
"message": "Mozilla formátumú stílus",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
@ -287,6 +415,14 @@
"message": "Csere",
"description": "Label before the replace input field in the editor shown on Ctrl-H"
},
"styleRegexpTestNone": {
"message": "Nincs illeszkedő fül",
"description": "RegExp test report: label for expressions that didn't match any tabs"
},
"importReportLegendUpdatedMeta": {
"message": "metainfó frissítve",
"description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles"
},
"appliesLabel": {
"message": "Amire érvényesül",
"description": "Label for 'applies to' fields on the edit/add screen"
@ -320,22 +456,66 @@
"message": "Az összes frissítés ellenőrzése és telepítése",
"description": ""
},
"filteredStyles": {
"message": "$numShown$ mutatva $numTotal$-ból/-ből",
"description": "TL note - make this message short",
"placeholders": {
"numTotal": {
"content": "$2"
},
"numShown": {
"content": "$1"
}
}
},
"importReportTitle": {
"message": "A stílusok importálása befejeződött",
"description": "Title of the report shown after importing styles"
},
"styleMozillaFormatHeading": {
"message": "Mozilla formátum",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
},
"cm_matchHighlight": {
"message": "Kijelöl",
"description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor."
},
"styleRegexpPartialExplanation": {
"message": "Ez a stílus olyan részlegesen illeszkedő reguláris kifejezéseket használ, melyek sértik a<a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4@dokumentumspecifikációt</a>, mely szerint egy teljes URL-illeszkedésre van szükség. Az érintett CSS-szekciók nem kerültek alkalmazásra az oldalon. Ez a stílus valószínűleg a Stylish Chrome-kiegészítőben lett létrehozva, amely helytelenül ellenőrzi a „regexp()” szabályokat az első verziótól fogva (ismert hiba).",
"description": ""
},
"styleBeautifyIndentConditional": {
"message": "@media, @supports behúzása",
"description": "CSS-beautifier option"
},
"unreachableContentScript": {
"message": "Nem sikerült az oldallal történő kommunikáció. Próbáld meg újratölteni az oldalt!",
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
},
"sectionRemove": {
"message": "Szekció eltávolítása",
"description": "Label for the button to remove a section"
},
"searchStylesTooltip": {
"message": "A URL alapján történő szűréshez írd előre, hogy „url:”\nPéldául: url:https://github.com/openstyles/stylus",
"description": "Label for the search filter textbox on the Manage styles page"
},
"disableAllStyles": {
"message": "Az összes stílus kikapcsolása",
"description": "Label for the checkbox that turns all enabled styles off."
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Lehet, hogy ezt a stílust helyileg szerkesztették",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
},
"undoGlobal": {
"message": "VIsszavonás mindegyik szekcióban",
"description": "CSS-beautify global Undo button label"
},
"optionsCustomizeIcon": {
"message": "Eszköztárikon",
"description": ""
},
"updateCompleted": {
"message": "A frissítés befejeződött.",
"description": "Text that displays when an update completed"
@ -344,14 +524,46 @@
"message": "Ellenőrzés...",
"description": "Text to display when checking a style for an update"
},
"styleRegexpTestFull": {
"message": "Illeszkedő fülek",
"description": "RegExp test report: label for the fully matching expressions"
},
"manageMaxTargets": {
"message": "Megjelenítendő célok száma",
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"manageFaviconsHelp": {
"message": "A Stylus egy külső szolgáltatást használ (https://www.google.com/s2/favicons)",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"updateCheckSkippedLocallyEdited": {
"message": "Ez a stílus helyileg lett szerkesztve.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
},
"optionsResetButton": {
"message": "Beállítások visszaállítása alapra",
"description": ""
},
"sectionCode": {
"message": "Kód",
"description": "Label for the code for a section"
},
"optionsAdvancedContextDelete": {
"message": "Delete hozzáadása a gyorsmenühöz",
"description": ""
},
"importReportLegendUpdatedBoth": {
"message": "metainformáció és kód frissítve",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
},
"cm_smartIndent": {
"message": "Intelligens behúzás használata",
"description": "Label for the checkbox controlling smart indentation option for the style editor."
},
"styleRegexpTestButton": {
"message": "RegExp teszt",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
},
"appliesHelp": {
"message": "Használd az \"A következőre érvényesül\" részt, hogy korlátozd, milyen URL-ekre vonatkozzon az itt lévő kód!",
"description": "Help text for 'applies to' section"
@ -360,6 +572,38 @@
"message": "Stílus szerkesztése",
"description": "Title of the page for editing styles"
},
"editDeleteText": {
"message": "Törlés",
"description": "Label for the context menu item in the editor to delete selected text"
},
"cm_autocompleteOnTyping": {
"message": "Automatikus kiegészítés gépeléskor",
"description": "Label for the checkbox in the style editor."
},
"manageOnlyDisabled": {
"message": "Csak letiltott stílusok",
"description": "Checkbox to show only disabled styles"
},
"stylusUnavailableForURLdetails": {
"message": "Biztonsági okokból a böngésző megtiltja, hogy a kiegészítők változtatásokat tegyenek a beépített oldalain (pl. chrome://verzió, a Chrome 61 alapértelmezett „új lap” oldala, about:addons és így tovább) valamint más kiterjesztések oldalain. Ezen kívül mindegyik böngésző korlátozza a saját kiegészítőgalériájának elérését (pl. Chrome webáruház vagy a Mozilla kiegészítők oldala)",
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
},
"cm_matchHighlightSelection": {
"message": "Csak kiválasztás",
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
},
"updateAllCheckSucceededSomeEdited": {
"message": "Egyes frissíthető stílusok nem lettek ellenőrizve, nehogy elvesszenek a helyi változtatások. A frissítéseket lehet erőltetni egyéni ellenőrzéssel külön-külön vagy az összes stílus ismételt ellenőrzésével (ez felülírja a helyi módosításokat).",
"description": "Text that displays when an update all check completed and no updates are available"
},
"stylusUnavailableForURL": {
"message": "A Stylus nem működik az ilyen oldalakon.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"manageOnlyUpdates": {
"message": "Csak frissíthetőek vagy problémásak",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
},
"addStyleTitle": {
"message": "Stílus hozzáadása",
"description": "Title of the page for adding styles"
@ -372,6 +616,10 @@
"message": "Hiba történt a Stylus adatbázisának használatakor. Szeretnéd meglátogatni a lehetséges megoldásokat tartalmazó weboldalt?",
"description": "Prompt when a DB error is encountered"
},
"optionsAdvanced": {
"message": "Haladó",
"description": ""
},
"importAppendTooltip": {
"message": "Az importált stílus hozzáadása a jelenlegi stílushoz",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -388,10 +636,22 @@
"message": "Gyorsgomb",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
},
"styleRegexpTestInvalid": {
"message": "Kihagyott érvénytelen reguláris kifejezések",
"description": "RegExp test report: label for the invalid expressions"
},
"manageOnlyExternal": {
"message": "Csak külső stílusok",
"description": "Checkbox to show only externally installed styles i.e. updatable"
},
"replaceAll": {
"message": "Az összes cseréje",
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
},
"importReportUnchanged": {
"message": "Semmi sem változott.",
"description": "Message in the report shown after importing styles"
},
"optionsActions": {
"message": "Műveletek",
"description": ""
@ -400,6 +660,10 @@
"message": "Sorra (vagy sor:oszlopra) ugrás",
"description": "Go to line or line:column on Ctrl-G in style code editor"
},
"manageOnlyLocalTooltip": {
"message": "(a stílusok nem egy userstyles.org oldalon lettek telepítve)",
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
},
"checkAllUpdates": {
"message": "Az összes stílus frissítésének ellenőrzése",
"description": "Label for the button to check all styles for updates"
@ -408,6 +672,14 @@
"message": "A beállítások felülete",
"description": "Go to Options UI"
},
"optionsCustomizeBadge": {
"message": "Jelvény az eszköztárikonon",
"description": ""
},
"importReportLegendIdentical": {
"message": "kihagyott egyezések",
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
},
"optionsPopupWidth": {
"message": "Felugró ablak szélessége (pixelben)",
"description": ""
@ -424,6 +696,18 @@
"message": "Billentyűműveletek",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"manageNewUI": {
"message": "Az új kezelési felületkiosztás",
"description": "Label for the checkbox that toggles the new UI on manage page"
},
"importReportUndoneTitle": {
"message": "Az importálás vissza lett vonva",
"description": "Title of the message box shown after undoing the import of styles"
},
"genericDisabledLabel": {
"message": "Letiltott",
"description": "Used in various lists/options to indicate that something is disabled"
},
"cm_indentWithTabs": {
"message": "Tabulátorok használata intelligens behúzásra",
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
@ -436,6 +720,14 @@
"message": "Törlés",
"description": "Label for the button to delete a style"
},
"updateCheckManualUpdateForce": {
"message": "Frissítés telepítése (a helyi módosítások felül lesznek írva)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"optionsAdvancedExposeIframes": {
"message": "iframe-ek kitevése HTML[stylus-iframe]-en keresztül",
"description": ""
},
"addStyleLabel": {
"message": "Új stílus írása",
"description": "Label for the button to go to the add style page"
@ -464,6 +756,10 @@
"message": "Válassz ki egy fájlt vagy húzd erre az oldalra!",
"description": "Message for backup"
},
"importReportUndone": {
"message": "stílusok visszavonva",
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
},
"helpKeyMapCommand": {
"message": "Írj be egy parancsot",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"

View File

@ -3,6 +3,10 @@
"message": "Tutto",
"description": "Text displayed for styles that apply to all sites"
},
"linterIssues": {
"message": "Problemi",
"description": "Label for the CSS linter issues block on the style edit page"
},
"defaultTheme": {
"message": "predefinito",
"description": "Default CodeMirror CSS theme option on the edit style page"
@ -19,6 +23,10 @@
"message": "Esporta",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "Installa",
"description": "Label for install button"
},
"linterJSONError": {
"message": "Formato JSON non valido",
"description": "Setting linter config with invalid JSON"
@ -36,13 +44,17 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Inserisci un nome.",
"message": "Inserisci un nome",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
"message": "Cronologia",
"description": "Used in various places to show a history log of something"
},
"shortcutsNote": {
"message": "Definisci scorciatoie da tastiera",
"description": ""
},
"appliesDomainOption": {
"message": "URL nel dominio",
"description": "Option to make the style apply to the entered string as a domain"
@ -55,6 +67,10 @@
"message": "Temi browser scuri",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "Importazione da formato Mozilla fallita",
"description": "Label for the import error"
},
"optionsOpenManager": {
"message": "Organizza stili",
"description": ""
@ -79,6 +95,10 @@
"message": "Cerca",
"description": "Label before the search input field in the editor shown on Ctrl-F"
},
"versionInvalidOlder": {
"message": "La versione dello stile è più vecchia di quella installata.",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "Si",
"description": "'Yes' button in a confirm dialog"
@ -115,6 +135,10 @@
"message": "Elimina",
"description": ""
},
"confirmDefault": {
"message": "Utilizza predefinito",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "Annulla",
"description": ""
@ -160,6 +184,10 @@
"message": "Aggiorna stili",
"description": ""
},
"manageText": {
"message": "<a href='https://userstyles.org'>Ottieni stili da userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Otteini aiuto</a>",
"description": "Help text on the manage page"
},
"manageOnlyLocal": {
"message": "Solo stili creati localmente",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
@ -176,6 +204,10 @@
"message": "Disattiva",
"description": "Label for the button to disable a style"
},
"prefShowBadge": {
"message": "Stili attivi per il sito attuale",
"description": "Label for the checkbox controlling toolbar badge text."
},
"styleCancelEditLabel": {
"message": "Torna a gestione",
"description": "Label for cancel button for style editing"
@ -188,6 +220,10 @@
"message": "Importa",
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
},
"shortcuts": {
"message": "Scorciatoie",
"description": "Go to shortcut configuration"
},
"updateCheckFailServerUnreachable": {
"message": "Aggiornamento non riuscito: server non raggiungibile.",
"description": "Text that displays when an update check failed because the update server is unreachable"
@ -216,6 +252,10 @@
"message": "Regexp non valida.",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "Licenza",
"description": "Label for the license"
},
"optionsHeading": {
"message": "Opzioni",
"description": "Heading for options section on manage page."
@ -237,6 +277,15 @@
"message": "Apri gestore stili",
"description": "Label for the style maanger opener in the browser action context menu."
},
"styleUpdate": {
"message": "Sei sicuro di voler aggiornare '$stylename$'?",
"description": "Confirmation when updating a style",
"placeholders": {
"stylename": {
"content": "$1"
}
}
},
"dysfunctional": {
"message": "Stylus non può funzionare in finestre anonime perché Firefox disattiva la connessione diretta al contesto della pagina di background interna dell'estensione.",
"description": "Displayed in Firefox when its settings make Stylus dysfunctional"
@ -266,6 +315,18 @@
"message": "URL che iniziano con",
"description": "Option to make the style apply to the entered string as a URL prefix"
},
"popupBordersTooltip": {
"message": "Utile per temi scuri nelle nuove versioni di Chrome dato che non colora più i bordi laterali",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "Forzare un aggiornamento sovrascriverà tutte le modifiche locali",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"installUpdateFromLabel": {
"message": "Controlla aggiornamenti",
"description": "Label for the checkbox to save current URL for update check"
},
"sectionHelp": {
"message": "Le sezioni consentono di definire diverse parti di codice da applicare a diversi insiemi di URL dello stesso stile. Ad esempio, un unico stile potrebbe modificare la home page di un sito diversamente da come modificherebbe il resto del sito.",
"description": "Help text for sections"
@ -290,6 +351,10 @@
"message": "Stile in formato Mozilla",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "Selezionatore colore per colori CSS",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "Scrivi stile per:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -327,6 +392,10 @@
"message": "Installa aggiornamento",
"description": "Label for the button to install an update for a single style"
},
"optionsCheckUpdate": {
"message": "Controlla e installa gli aggiornamenti disponibili",
"description": ""
},
"filteredStyles": {
"message": "$numShown$ mostrati di $numTotal$ totali",
"description": "TL note - make this message short",
@ -339,6 +408,10 @@
}
}
},
"importReportTitle": {
"message": "Importazione stili terminata",
"description": "Title of the report shown after importing styles"
},
"styleMozillaFormatHeading": {
"message": "Formato Mozilla",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
@ -355,6 +428,10 @@
"message": "Disattiva tutti gli stili",
"description": "Label for the checkbox that turns all enabled styles off."
},
"undoGlobal": {
"message": "Annulla in tutte le sezioni",
"description": "CSS-beautify global Undo button label"
},
"updateCompleted": {
"message": "Aggiornamento completato.",
"description": "Text that displays when an update completed"
@ -375,6 +452,21 @@
"message": "Stylus utilizza un servizio esterno https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "'$stylename$' è già installato. Vuoi sovrascriverlo?\nVersione: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "Questo stile è stato editato localmente.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -391,6 +483,18 @@
"message": "Codice",
"description": "Label for the code for a section"
},
"optionsAdvancedContextDelete": {
"message": "Aggiungi 'Elimina' al menu contestuale dell'editor",
"description": ""
},
"configureStyle": {
"message": "Configura",
"description": "Label for the button to configure userstyle"
},
"cm_smartIndent": {
"message": "Usa indentazione intelligente",
"description": "Label for the checkbox controlling smart indentation option for the style editor."
},
"styleRegexpTestButton": {
"message": "Test RegExp",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
@ -407,18 +511,47 @@
"message": "Elimina",
"description": "Label for the context menu item in the editor to delete selected text"
},
"cm_autocompleteOnTyping": {
"message": "Completamento automatico durante digitazione",
"description": "Label for the checkbox in the style editor."
},
"styleMetaErrorColor": {
"message": "$color$ non è un colore valido",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"manageOnlyDisabled": {
"message": "Solo stili disattivati",
"description": "Checkbox to show only disabled styles"
},
"cm_matchHighlightSelection": {
"message": "Solo selezione",
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
},
"stylusUnavailableForURL": {
"message": "Stylus non funziona in pagine come questa.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "Aggiungi bordi bianchi sui lati",
"description": ""
},
"manageOnlyUpdates": {
"message": "Solo con aggiornamenti o problemi",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
},
"addStyleTitle": {
"message": "Aggiunta di stili",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "Link esterno",
"description": "Label for external links"
},
"importReplaceLabel": {
"message": "Sovrascrivi stile",
"description": "Label for the button to import and overwrite current style"
@ -427,6 +560,14 @@
"message": "Avanzato",
"description": ""
},
"alphaChannel": {
"message": "Opacità",
"description": "Label of color's opacity"
},
"editorStylesButton": {
"message": "Cerca stili editor",
"description": "Find styles for the editor"
},
"optionsOpen": {
"message": "Apri",
"description": ""
@ -455,10 +596,39 @@
"message": "Opzioni UI",
"description": "Go to Options UI"
},
"installUpdateFrom": {
"message": "Attualmente lo stile è aggiornato da $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"optionsPopupWidth": {
"message": "Larghezza popup (in pixel)",
"description": ""
},
"installButtonReinstall": {
"message": "Reinstalla",
"description": "Label for reinstall button"
},
"appliesLineWidgetWarning": {
"message": "Non funziona con CSS minificato",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "Annulla",
"description": "Button label"
},
"externalSupport": {
"message": "Supporto",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "Salva",
"description": "'Save' button in a confirm dialog"
},
"genericDisabledLabel": {
"message": "Disattivato",
"description": "Used in various lists/options to indicate that something is disabled"
@ -471,6 +641,19 @@
"message": "Elimina",
"description": "Label for the button to delete a style"
},
"updateCheckManualUpdateForce": {
"message": "Installa aggiornamento (le modifiche locali verranno sovrascritte)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "Installazione stile fallita!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"addStyleLabel": {
"message": "Scrivi nuovo stile",
"description": "Label for the button to go to the add style page"
@ -479,6 +662,10 @@
"message": "Per disattivare il controllo automatico degli aggiornamenti degli stili, imposta l'intervallo a 0",
"description": ""
},
"installButtonUpdate": {
"message": "Aggiorna",
"description": "Label for update button"
},
"manageOnlyEnabled": {
"message": "Solo stili attivati",
"description": "Checkbox to show only enabled styles"
@ -487,6 +674,14 @@
"message": "Modifica",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "Installato",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "Autore",
"description": "Label for the style author"
},
"cm_theme": {
"message": "Tema",
"description": "Label for the style editor's CSS theme."
@ -499,6 +694,10 @@
"message": "Seleziona un file o trascinalo in questa pagina.",
"description": "Message for backup"
},
"importReportUndone": {
"message": "gli stili sono stati ripristinati",
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
},
"helpKeyMapCommand": {
"message": "Inserisci il nome di un comando",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
@ -506,5 +705,9 @@
"description": {
"message": "Modifica lo stile del Web con Stylus, un gestore di stili utente. Stylus consente di installare facilmente temi e skin per Google, Facebook, YouTube, Orkut e moltissimi altri siti.",
"description": "Extension description"
},
"confirmClose": {
"message": "Chiudi",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -24,13 +24,21 @@
"description": ""
},
"styleEnabledToggleHint": {
"message": "Alt-Enterを押して、有効/無効の状態を切り替え、スタイルを保存します",
"message": "Alt-Enterを押すと、有効/無効の状態を切り替え、スタイルを保存します",
"description": "Help text for the '[x] enable' checkbox in the editor"
},
"exportLabel": {
"message": "エクスポート",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "インストール",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "不正な @var チェックボックス: 値は0か1である必要があります",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "不正なJSONフォーマットです",
"description": "Setting linter config with invalid JSON"
@ -52,7 +60,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "名前を入力してください",
"message": "名前を入力してください",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
@ -83,6 +91,10 @@
"message": "暗いブラウザのテーマ",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "Mozilla形式のインポートに失敗しました",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "スタイルに追加",
"description": "Label for the button to import a style and append to the existing sections"
@ -119,6 +131,10 @@
"message": "グレー表示",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "インストール済みのスタイルより古いバージョンです。",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "はい",
"description": "'Yes' button in a confirm dialog"
@ -148,7 +164,7 @@
"description": "Icon tooltip to indicate that it opens a popup with the selected linter configuration"
},
"styleToMozillaFormatHelp": {
"message": "Mozilla 形式のコードは、userstyles.org に投稿することができ、また従来の Stylish for Firefox でも使用できます",
"message": "Mozilla 形式のコードは、userstyles.org に投稿することができ、また従来の Stylish for Firefox でも使用できます",
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
},
"sectionAdd": {
@ -171,10 +187,18 @@
"message": "削除",
"description": ""
},
"confirmDefault": {
"message": "デフォルトを使用",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "キャンセル",
"description": ""
},
"cm_autoCloseBracketsTooltip": {
"message": "()[]{}''\"\" の開き括弧/引用符の入力時に、対応する閉じ括弧/引用符を自動的に追加します",
"description": "Label for the checkbox in the style editor."
},
"retrieveBckp": {
"message": "スタイルをインポート",
"description": ""
@ -200,7 +224,7 @@
"description": "Option to make the style apply to the entered string as a regular expression"
},
"optionsAdvancedExposeIframesNote": {
"message": "'html[stylus-iframe] h1 { display:none }' のようにiframe用のCSSを書くことができるようになります。",
"message": "「 html[stylus-iframe] h1 { display:none } 」のようにiframe用のCSSを書くことができるようになります。",
"description": ""
},
"importReportLegendUpdatedCode": {
@ -232,6 +256,15 @@
"message": "ローカル作成スタイルのみ",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"styleMetaErrorPreprocessor": {
"message": "未サポートの @preprocessor: $preprocessor$",
"description": "Error displayed when the value of @preprocessor is not supported",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"linterIssuesHelp": {
"message": "$link$ によってこれらの問題点が見つかりました :",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
@ -241,6 +274,10 @@
}
}
},
"parseUsercssError": {
"message": "Stylusはusercssの解析に失敗しました: ",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "コンテンツの検索",
"description": "Label for the search filter textbox on the Manage styles page"
@ -253,6 +290,10 @@
"message": "全スタイルを再チェックします。私はスタイルを編集していません!",
"description": "Label for the button to apply all detected updates"
},
"liveReloadLabel": {
"message": "自動リロード",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "chrome://extensions ページでStylus拡張機能の当該チェックボックスを有効にした場合にのみ、Stylusは file:// URLにアクセスできます。",
"description": "Note in the toolbar popup for file:// URLs"
@ -325,6 +366,10 @@
"message": "正規表現は無効です。",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "ライセンス",
"description": "Label for the license"
},
"optionsHeading": {
"message": "オプション",
"description": "Heading for options section on manage page."
@ -367,6 +412,10 @@
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "新しいスタイルを usercss として作成します",
"description": ""
},
"genericResetLabel": {
"message": "リセット",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -381,7 +430,7 @@
}
},
"updateCheckSucceededNoUpdate": {
"message": "スタイルは最新の状態です",
"message": "スタイルは最新の状態です",
"description": "Text that displays when an update check completed and no update is available"
},
"appliesUrlPrefixOption": {
@ -396,6 +445,10 @@
"message": "正規表現検索に /re/ シンタックスを使用する",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "新しいChromeが横の境界線を描かなくなったため、暗いテーマで有効です",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "更新を強制すると、すべてのローカルでの編集内容が上書きされます。",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -408,6 +461,10 @@
"message": "現在のスタイルの内容を破棄し、インポートされたスタイルで上書きする",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "更新をチェック",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "ダブルクリックで高さを最大化/元に戻す",
"description": "Tooltip for the resize grip in style editor"
@ -425,7 +482,7 @@
"description": "Text displayed when no styles are installed for the current site"
},
"appliesDisplayTruncatedSuffix": {
"message": "サイトを追加",
"message": "さらに表示",
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
},
"appliesRemove": {
@ -440,6 +497,10 @@
"message": "Mozilla形式のスタイル",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "CSSの色選択ツール",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "次のスタイルを書く:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -510,7 +571,7 @@
"description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor."
},
"styleRegexpPartialExplanation": {
"message": "このスタイルは、完全なURLの一致を要求する <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document 仕様</a> に違反した部分一致の正規表現を使用しています。影響を受けるCSSセクションはページに適用されませんでした。このスタイルは、'regexp()' のルールを誤ってチェックしていた かなり初期のバージョンの Stylish-for-Chrome で作成された可能性があります(既知のバグです)",
"message": "このスタイルは、完全なURLの一致を要求する <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document 仕様</a> に違反した部分一致の正規表現を使用しています。影響を受けるCSSセクションはページに適用されませんでした。このスタイルは、'regexp()' のルールを誤ってチェックしていた かなり初期のバージョンの Stylish-for-Chrome で作成された可能性があります(既知のバグです)",
"description": ""
},
"linterCSSLintSettings": {
@ -537,6 +598,10 @@
"message": "すべてのスタイルをオフにする",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "「適用先」の情報を表示",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "このスタイルはローカルで編集されている可能性があります。",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -550,7 +615,7 @@
"description": ""
},
"updateCompleted": {
"message": "更新が完了しました",
"message": "更新が完了しました",
"description": "Text that displays when an update completed"
},
"dysfunctionalBackgroundConnection": {
@ -570,13 +635,28 @@
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"popupManageTooltip": {
"message": "Shift-クリック または 右クリック で、現在のサイトに適用可能なスタイルの管理画面を開きます",
"message": "Shift-クリック または 右クリック で、現在のサイトに適用可能なスタイルの管理画面を開きます",
"description": "Tooltip for the 'Manage' button in the popup."
},
"manageFaviconsHelp": {
"message": "Stylusは外部サービスを使用します https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "「$stylename$」はすでにインストール済みです。上書きしますか?\nバージョン: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "このスタイルはローカルで編集されました。",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -585,6 +665,10 @@
"message": "ルールの完全なリストを見る",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "このスタイルはエディタの外部で変更されました。このスタイルをリロードしますか?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "オプションをリセット",
"description": ""
@ -593,6 +677,10 @@
"message": "コード",
"description": "Label for the code for a section"
},
"externalUsercssDocument": {
"message": "Usercssの文書",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "エディタのコンテキストメニューに「削除」を追加します",
"description": ""
@ -606,6 +694,10 @@
}
}
},
"configureStyle": {
"message": "設定",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "件のメタ情報とコードを更新しました",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -634,12 +726,38 @@
"message": "入力の自動補完",
"description": "Label for the checkbox in the style editor."
},
"linterCSSLintIncompatible": {
"message": "CSSLintは「$preprocessorname$」プリプロセッサをサポートしていません",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"styleMetaErrorSelectValueMismatch": {
"message": "不正な @select: 値がリストに存在しません",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ は有効な色ではありません",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"externalFeedback": {
"message": "フィードバック",
"description": "Label for the external link to send feedback for the style"
},
"manageOnlyDisabled": {
"message": "無効なスタイルのみ",
"description": "Checkbox to show only disabled styles"
},
"stylusUnavailableForURLdetails": {
"message": "セキュリティ上の予防措置として、ブラウザは拡張機能がビルトイン ページchrome://version, Chrome 61 の標準の新しいタブページ, about:addons 等に影響を与えることを禁止しています。これは他の拡張機能のページについても同様です。各ブラウザはまた、自身の拡張機能ギャラリーへのアクセスについても制限していますChrome Web ストア や AMO 等)",
"message": "セキュリティ上の予防措置として、ブラウザは拡張機能がビルトイン ページchrome://version, Chrome 61 の標準の新しいタブページ, about:addons 等に影響を与えることを禁止しています。これは他の拡張機能のページについても同様です。各ブラウザはまた、自身の拡張機能ギャラリーへのアクセスについても制限していますChrome Web ストア や AMO 等)",
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
},
"cm_matchHighlightSelection": {
@ -654,6 +772,10 @@
"message": "このようなページではStylusは動作しません。",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "横に白い境界線を追加する",
"description": ""
},
"manageOnlyUpdates": {
"message": "更新または問題があるスタイルのみ",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -662,6 +784,14 @@
"message": "スタイルを追加",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "外部リンク",
"description": "Label for external links"
},
"externalHomepage": {
"message": "ホームページ",
"description": "Label for the external link to style's homepage"
},
"importReplaceLabel": {
"message": "スタイルを上書き",
"description": "Label for the button to import and overwrite current style"
@ -678,6 +808,10 @@
"message": "上級者向け",
"description": ""
},
"alphaChannel": {
"message": "透明度",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "インポートされたスタイルを現在のスタイルに追加する",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -707,7 +841,7 @@
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
},
"importReportUnchanged": {
"message": "変更がありませんでした",
"message": "変更がありませんでした",
"description": "Message in the report shown after importing styles"
},
"optionsActions": {
@ -730,10 +864,23 @@
"message": "オプション UI",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "色選択ツールを開きます",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "ツールバーアイコンのバッジ",
"description": ""
},
"installUpdateFrom": {
"message": "現在、このスタイルは「$url$」から更新されます",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"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"
@ -742,6 +889,14 @@
"message": "ポップアップの幅(ピクセル単位)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "括弧と引用符を自動的に閉じる",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "再インストール",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "これらの不正な設定のために保存されませんでした :",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
@ -750,6 +905,19 @@
"message": "いいえ",
"description": "'No' button in a confirm dialog"
},
"styleMissingMeta": {
"message": "メタデータがありません @$key$",
"description": "Error displayed when a mandatory metadata is missing",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"appliesLineWidgetWarning": {
"message": "圧縮されたCSSでは機能しません",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "元に戻す",
"description": "Button label"
@ -758,6 +926,14 @@
"message": "キーマップ",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "サポート",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "保存",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "新しい管理UIレイアウト",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -778,6 +954,10 @@
"message": "次に置換",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"liveReloadError": {
"message": "ファイルの監視中にエラーが発生しました",
"description": "The label of live-reload error"
},
"deleteStyleLabel": {
"message": "削除",
"description": "Label for the button to delete a style"
@ -786,6 +966,15 @@
"message": "更新をインストール(ローカルでの編集内容は上書きされます)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "ユーザースタイルのインストールに失敗しました!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "HTML[stylus-iframe] によってiframeへのアクセスを可能にします",
"description": ""
@ -798,6 +987,10 @@
"message": "ユーザースタイルの自動更新チェックを無効にするには、間隔を0に設定します。",
"description": ""
},
"installButtonUpdate": {
"message": "更新",
"description": "Label for update button"
},
"backupButtons": {
"message": "バックアップ",
"description": "Heading for backup"
@ -810,6 +1003,14 @@
"message": "編集",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "インストール済み",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "作者",
"description": "Label for the style author"
},
"cm_theme": {
"message": "テーマ",
"description": "Label for the style editor's CSS theme."
@ -818,6 +1019,10 @@
"message": "新しいウィンドウでエディタを開く",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "「適用先」の最後の項目を削除することはできません",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "ファイルを選択するか、このページにドラッグ&ドロップします。",
"description": "Message for backup"
@ -837,5 +1042,9 @@
"description": {
"message": "Stylus でウェブのデザインを変更しましょう。これは、ユーザースタイルを管理するツールです。Stylus を利用すると、多くの人気サイト向けのテーマやスキンを簡単にインストールできます。",
"description": "Extension description"
},
"confirmClose": {
"message": "閉じる",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -24,7 +24,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Vul een naam in.",
"message": "Vul een naam in",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -19,6 +19,10 @@
"message": "Eksportuj style",
"description": ""
},
"manageOnlyUsercss": {
"message": "Tylko style Usercss",
"description": "Checkbox to show only Usercss styles"
},
"optionsUpdateInterval": {
"message": "Automatycznie sprawdzaj i zainstaluj wszystkie dostępne aktualizacje stylów użytkownika (w godzinach)",
"description": ""
@ -31,6 +35,14 @@
"message": "Eksportuj",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "Zainstaluj",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "Nieprawidłowe pole @var: wartość musi wynosić 0 lub 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "Nieprawidłowy format JSON",
"description": "Setting linter config with invalid JSON"
@ -44,7 +56,7 @@
"description": ""
},
"cm_tabSize": {
"message": "Rozmiar tabulatora",
"message": "Szerokość tabulacji",
"description": "Label for the text box controlling tab size option for the style editor."
},
"enableStyleLabel": {
@ -52,7 +64,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Wprowadź nazwę.",
"message": "Wpisz nazwę",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
@ -75,6 +87,10 @@
"message": "Styl nie został zastosowany z powodu nieprawidłowego użycia 'regexp()'",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"colorpickerSwitchFormatTooltip": {
"message": "Zmiana formatu: HEX -> RGB -> HSL.\nShift i kliknięcie do odwrócenia kierunku.\nTakże klawiszami PgUp (PageUp), PgDn (PageDown).",
"description": "Tooltip for the switch button in the color picker popup in the style editor."
},
"styleRegexpInvalidExplanation": {
"message": "Niektóre reguły 'regexp()', których nie można było w ogóle skompilować.",
"description": ""
@ -83,6 +99,10 @@
"message": "Ciemne motywy przeglądarki",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "Nie udało się zaimportować z formatu Mozilla",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "Dołącz do stylu",
"description": "Label for the button to import a style and append to the existing sections"
@ -119,6 +139,10 @@
"message": "Wyszarzone",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "Wersja jest starsza niż zainstalowany styl.",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "Tak",
"description": "'Yes' button in a confirm dialog"
@ -171,10 +195,18 @@
"message": "Usuń",
"description": ""
},
"confirmDefault": {
"message": "Użyj domyślnych",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "Anuluj",
"description": ""
},
"cm_autoCloseBracketsTooltip": {
"message": "Automatycznie dodawaj zamknięcie pary podczas pisania jednego z otwarcia ()[]{}''\"\"",
"description": "Label for the checkbox in the style editor."
},
"retrieveBckp": {
"message": "Importuj style",
"description": ""
@ -232,6 +264,15 @@
"message": "Tylko style stworzone lokalnie",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"styleMetaErrorPreprocessor": {
"message": "Nieobsługiwany @preprocessor: $preprocessor$",
"description": "Error displayed when the value of @preprocessor is not supported",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"linterIssuesHelp": {
"message": "Te problemy zostały znalezione przez $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
@ -241,6 +282,10 @@
}
}
},
"parseUsercssError": {
"message": "Stylusowi nie udało się sparsować usercss:",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "Szukaj treści",
"description": "Label for the search filter textbox on the Manage styles page"
@ -253,6 +298,14 @@
"message": "Sprawdź ponownie, nie edytowałem żadnych stylów!",
"description": "Label for the button to apply all detected updates"
},
"manageOnlyNonUsercss": {
"message": "Tylko style nie-Usercss",
"description": "Checkbox to show only non-Usercss (standard) styles"
},
"liveReloadLabel": {
"message": "Przeładuj na żywo",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "Stylus może uzyskać dostęp do adresów URL file:// tylko po włączeniu odpowiedniego pola wyboru rozszerzenia Stylus na stronie chrome://extensions.",
"description": "Note in the toolbar popup for file:// URLs"
@ -325,6 +378,10 @@
"message": "Regexp jest nieprawidłowe.",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "Licencja",
"description": "Label for the license"
},
"optionsHeading": {
"message": "Opcje",
"description": "Heading for options section on manage page."
@ -367,6 +424,10 @@
"message": "nieprawidłowe pominięte",
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Napisz nowy styl jako usercss",
"description": ""
},
"genericResetLabel": {
"message": "Resetuj",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -396,6 +457,10 @@
"message": "Użyj składni /re/ dla wyszukiwania regexp",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "Przydaje się do ciemnych motywów w nowym Chrome, ponieważ nie maluje już bocznych krawędzi",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "Wymuszenie aktualizacji spowoduje nadpisanie wszelkich lokalnych zmian.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -408,6 +473,10 @@
"message": "Odrzuć zawartość bieżącego stylu i nadpisz go przy użyciu importowanego stylu",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "Sprawdź aktualizacje",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "Kliknij dwukrotnie, aby zmaksymalizować lub przywrócić wysokość",
"description": "Tooltip for the resize grip in style editor"
@ -440,6 +509,10 @@
"message": "Styl w formacie Mozilla",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "Próbniki kolorów CSS",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "Napisz styl dla:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -541,6 +614,10 @@
"message": "Wyłącz wszystkie style",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "Wyświetl informacje 'Dotyczy'",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Ten styl mógł być edytowany lokalnie.",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -581,6 +658,21 @@
"message": "Stylus korzysta z usługi zewnętrznej https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "'$stylename$' jest już zainstalowany. Zastąpić?\nWersja: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "Ten styl był edytowany lokalnie.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -589,6 +681,10 @@
"message": "Zobacz pełną listę reguł",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "Styl zmieniono poza edytorem. Czy chcesz przeładować styl?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "Resetuj opcje",
"description": ""
@ -597,6 +693,10 @@
"message": "Kod",
"description": "Label for the code for a section"
},
"externalUsercssDocument": {
"message": "Dokumentacja Usercss",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "Dodaj 'Usuń' do menu kontekstowego edytora",
"description": ""
@ -610,6 +710,10 @@
}
}
},
"configureStyle": {
"message": "Skonfiguruj",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "zaktualizowano meta info i kod",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -638,6 +742,32 @@
"message": "Autouzupełnianie podczas pisania",
"description": "Label for the checkbox in the style editor."
},
"linterCSSLintIncompatible": {
"message": "CSSLint nie obsługuje preprocesora $preprocessorname$",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"styleMetaErrorSelectValueMismatch": {
"message": "Nieprawidłowy @select: wartość nie istnieje na liście",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ nie jest prawidłowym kolorem",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"externalFeedback": {
"message": "Informacje zwrotne",
"description": "Label for the external link to send feedback for the style"
},
"manageOnlyDisabled": {
"message": "Tylko wyłączone style",
"description": "Checkbox to show only disabled styles"
@ -662,6 +792,10 @@
"message": "Stylus nie działa na takich stronach.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "Dodaj białe obramowania po bokach",
"description": ""
},
"manageOnlyUpdates": {
"message": "Tylko z aktualizacjami lub problemami",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -670,6 +804,14 @@
"message": "Dodaj styl",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "Link zewnętrzny",
"description": "Label for external links"
},
"externalHomepage": {
"message": "Strona główna",
"description": "Label for the external link to style's homepage"
},
"importReplaceLabel": {
"message": "Nadpisz styl",
"description": "Label for the button to import and overwrite current style"
@ -686,6 +828,10 @@
"message": "Zaawansowane",
"description": ""
},
"alphaChannel": {
"message": "Nieprzezroczystość",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "Dołącz importowany styl do bieżącego stylu",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -742,18 +888,43 @@
"message": "Opcje interfejsu",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "Otwórz próbnik kolorów",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "Emblemat na ikonie paska narzędzi",
"description": ""
},
"installUpdateFrom": {
"message": "Obecnie styl jest aktualizowany z $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"message": "identyczne pominięte",
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
},
"manageNewStyleAsUsercss": {
"message": "jako Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
},
"optionsPopupWidth": {
"message": "Szerokość okna (w pikselach)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "Automatyczne zamykanie nawiasów i cytatów",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "Przeinstaluj",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "Nie zapisano z powodu tych nieprawidłowych ustawień konfiguracji:",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
@ -762,6 +933,19 @@
"message": "Nie",
"description": "'No' button in a confirm dialog"
},
"styleMissingMeta": {
"message": "Brakujące metadane @$key$",
"description": "Error displayed when a mandatory metadata is missing",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"appliesLineWidgetWarning": {
"message": "Nie działa ze zminifikowanym CSS",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "Cofnij",
"description": "Button label"
@ -770,6 +954,14 @@
"message": "Mapa klawiszy",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "Wsparcie",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "Zapisz",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "Nowy układ interfejsu zarządzania",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -790,6 +982,10 @@
"message": "Zamień na",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"liveReloadError": {
"message": "Wystąpił błąd podczas oglądania pliku",
"description": "The label of live-reload error"
},
"deleteStyleLabel": {
"message": "Usuń",
"description": "Label for the button to delete a style"
@ -798,6 +994,15 @@
"message": "Zainstaluj aktualizację (lokalne zmiany zostaną nadpisane)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "Nie udało się zainstalować stylu!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "Odsłoń ramki pływające za pomocą HTML[stylus-iframe]",
"description": ""
@ -810,6 +1015,10 @@
"message": "Aby wyłączyć automatyczne sprawdzanie aktualizacji stylów użytkownika, ustaw interwał na 0",
"description": ""
},
"installButtonUpdate": {
"message": "Zaktualizuj",
"description": "Label for update button"
},
"backupButtons": {
"message": "Kopia zapasowa",
"description": "Heading for backup"
@ -822,6 +1031,14 @@
"message": "Edytuj",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "Zainstalowany",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "Autor",
"description": "Label for the style author"
},
"cm_theme": {
"message": "Motyw",
"description": "Label for the style editor's CSS theme."
@ -830,6 +1047,10 @@
"message": "Otwórz edytor w nowym oknie",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "Nie można usunąć ostatniego wpisu 'dotyczy'",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "Wybierz plik lub przeciągnij i upuść go na tę stronę.",
"description": "Message for backup"
@ -849,5 +1070,9 @@
"description": {
"message": "Przeprojektuj sieć za pomocą Stylusa menedżera stylów użytkownika. Stylus umożliwia łatwe instalowanie motywów i skórek dla wielu popularnych stron.",
"description": "Extension description"
},
"confirmClose": {
"message": "Zamknij",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -8,7 +8,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Insira um nome.",
"message": "Insira um nome",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -44,7 +44,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Insira um nome.",
"message": "Insira um nome",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {

View File

@ -19,6 +19,10 @@
"message": "Экспорт стилей",
"description": ""
},
"manageOnlyUsercss": {
"message": "Только Usercss стили",
"description": "Checkbox to show only Usercss styles"
},
"optionsUpdateInterval": {
"message": "Автоматически проверять и устанавливать обновления стилей (интервал в часах)",
"description": ""
@ -31,6 +35,14 @@
"message": "Экспорт",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "Установить",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "Ошибочный @var checkbox: значение должно быть 0 или 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "Ошибка формата JSON",
"description": "Setting linter config with invalid JSON"
@ -52,7 +64,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Введите название.",
"message": "Введите название",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
@ -75,6 +87,10 @@
"message": "Стиль не был применен из-за некорректного regexp()",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"colorpickerSwitchFormatTooltip": {
"message": "Сменить формат: #FFF -> RGB -> HSL.\nКлик с Shift меняет в обратном направлении.\nТакже клавишами PgUp, PgDn (\"Страница вверх\" и \"Страница вниз\").",
"description": "Tooltip for the switch button in the color picker popup in the style editor."
},
"styleRegexpInvalidExplanation": {
"message": "Некоторые 'regexp()' выражения не удалось скомпилировать.",
"description": ""
@ -83,6 +99,10 @@
"message": "Тёмный интерфейс браузера",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "Ошибка импорта формата Mozilla",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "Добавить к стилю",
"description": "Label for the button to import a style and append to the existing sections"
@ -123,6 +143,10 @@
"message": "Обесцвечивать",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "Версия меньше установленной.",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "Да",
"description": "'Yes' button in a confirm dialog"
@ -175,10 +199,18 @@
"message": "Удалить",
"description": ""
},
"confirmDefault": {
"message": "По умолчанию",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "Отмена",
"description": ""
},
"cm_autoCloseBracketsTooltip": {
"message": "Автоматически вставлять закрывающий символ при наборе открывающего из ()[]{}''\"\"",
"description": "Label for the checkbox in the style editor."
},
"retrieveBckp": {
"message": "Импорт стилей",
"description": ""
@ -236,6 +268,15 @@
"message": "Только локально созданные стили",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"styleMetaErrorPreprocessor": {
"message": "Ошибочный @preprocessor: $preprocessor$",
"description": "Error displayed when the value of @preprocessor is not supported",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"linterIssuesHelp": {
"message": "Проблемы, найденные правилами $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
@ -245,6 +286,10 @@
}
}
},
"parseUsercssError": {
"message": "Stylus не смог распарсить usercss:",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "Искать по содержимому",
"description": "Label for the search filter textbox on the Manage styles page"
@ -257,6 +302,14 @@
"message": "Я не менял стили, проверить заново!",
"description": "Label for the button to apply all detected updates"
},
"manageOnlyNonUsercss": {
"message": "Спрятать Usercss стили",
"description": "Checkbox to show only non-Usercss (standard) styles"
},
"liveReloadLabel": {
"message": "Автозагрузка изменений",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "Доступ к адресам file:// возможен только если включить соответствующую опцию для Stylus на странице chrome://extensions.",
"description": "Note in the toolbar popup for file:// URLs"
@ -329,6 +382,10 @@
"message": "Ошибка в регулярном выражении.",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "Лицензия",
"description": "Label for the license"
},
"optionsHeading": {
"message": "Настройки",
"description": "Heading for options section on manage page."
@ -371,6 +428,10 @@
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Создавать стили в формате usercss",
"description": ""
},
"genericResetLabel": {
"message": "Сброс",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -400,6 +461,10 @@
"message": "Используйте нотацию /re/ для поиска регулярными выражениями",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "Полезно для темных тем оформления т.к. новый Chrome не рисует боковые бордюры",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "Форсирование обновления удалит локальные изменения.",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -412,6 +477,10 @@
"message": "Удалить содержимое редактируемого стиля и заменить его на импортируемый стиль",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "Проверить обновления",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "Двойной клик = переключить максимальный размер",
"description": "Tooltip for the resize grip in style editor"
@ -444,6 +513,10 @@
"message": "Стиль в формате Mozilla",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "Раскрашивать цвета в CSS",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "Создать стиль для:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -545,6 +618,10 @@
"message": "Выключить все стили",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "Показать целевые сайты секций",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Возможно был изменен локально.",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -585,6 +662,21 @@
"message": "Используется сторонний сервис https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "'$stylename$' уже установлен. Обновить?\nВерсии: $oldVersion$ -> $newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "Стиль был изменен локально.",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -593,6 +685,10 @@
"message": "Открыть полный список правил",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "Стиль был изменен вне редактора. Перезагрузить?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "Сброс настроек",
"description": ""
@ -601,6 +697,10 @@
"message": "Код",
"description": "Label for the code for a section"
},
"externalUsercssDocument": {
"message": "Документация по usercss",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "Показывать команду \"Удалить\" в контекстном меню редактора",
"description": ""
@ -614,6 +714,10 @@
}
}
},
"configureStyle": {
"message": "Настроить",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "обновлены мета-данные и код",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -642,6 +746,32 @@
"message": "Подсказки при наборе кода",
"description": "Label for the checkbox in the style editor."
},
"linterCSSLintIncompatible": {
"message": "CSSLint не поддерживает препроцессор $preprocessorname$",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"styleMetaErrorSelectValueMismatch": {
"message": "Ошибочный @select: значение не в списке",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ - цвет не распознан",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"externalFeedback": {
"message": "Отзывы",
"description": "Label for the external link to send feedback for the style"
},
"manageOnlyDisabled": {
"message": "Только неактивные стили",
"description": "Checkbox to show only disabled styles"
@ -662,6 +792,10 @@
"message": "Такие адреса не поддерживаются.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "Добавить белый бордюр по бокам",
"description": ""
},
"manageOnlyUpdates": {
"message": "Только обновляемые и проблемные",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -670,6 +804,14 @@
"message": "Добавление стиля",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "Внешняя ссылка",
"description": "Label for external links"
},
"externalHomepage": {
"message": "Домашняя страницы",
"description": "Label for the external link to style's homepage"
},
"importReplaceLabel": {
"message": "Заменить стиль",
"description": "Label for the button to import and overwrite current style"
@ -686,6 +828,10 @@
"message": "Другое",
"description": ""
},
"alphaChannel": {
"message": "Прозрачность",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "Добавить импортируемый стиль к редактируемому",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -742,18 +888,43 @@
"message": "Настройки",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "Изменить цвет",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "Бейдж на пиктограмме в тулбаре",
"description": ""
},
"installUpdateFrom": {
"message": "Источник обновления стиля: $url$",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"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"
},
"manageNewStyleAsUsercss": {
"message": "в формате Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
},
"optionsPopupWidth": {
"message": "Ширина всплывающего окна (в пикселах)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "Закрывать скобки/кавычки при наборе",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "Переустановить",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "Не сохранено из-за неправильных настроек ниже:",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
@ -762,6 +933,19 @@
"message": "Нет",
"description": "'No' button in a confirm dialog"
},
"styleMissingMeta": {
"message": "Отсутствуют метаданные @$key$",
"description": "Error displayed when a mandatory metadata is missing",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"appliesLineWidgetWarning": {
"message": "Не работает с минифицированным CSS",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "Отменить",
"description": "Button label"
@ -770,6 +954,14 @@
"message": "Раскладка",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "Поддержка",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "Сохранить",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "Новый интерфейс",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -790,6 +982,10 @@
"message": "Заменить на",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"liveReloadError": {
"message": "Ошибка слежения за файлом",
"description": "The label of live-reload error"
},
"deleteStyleLabel": {
"message": "Удалить",
"description": "Label for the button to delete a style"
@ -798,6 +994,15 @@
"message": "Установить обновление (локальные изменения будут утеряны)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "Ошибка установки стиля!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "Выявлять iframe путем добавления HTML[stylus-iframe]",
"description": ""
@ -810,6 +1015,10 @@
"message": "Для отключения автоматического обновления стилей введите 0 (ноль)",
"description": ""
},
"installButtonUpdate": {
"message": "Обновить",
"description": "Label for update button"
},
"backupButtons": {
"message": "Резервное копирование",
"description": "Heading for backup"
@ -822,6 +1031,14 @@
"message": "Изменить",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "Установлен",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "Автор",
"description": "Label for the style author"
},
"cm_theme": {
"message": "Тема",
"description": "Label for the style editor's CSS theme."
@ -830,6 +1047,10 @@
"message": "Открывать редактор в новом окне",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "Нельзя удалить последний элемент",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "Нажмите Импорт чтобы выбрать файл или просто перетащите его на эту страницу",
"description": "Message for backup"
@ -849,5 +1070,9 @@
"description": {
"message": "Настраивайте стили веб-сайтов с помощью менеджера стилей Stylus. Он позволяет легко установить темы и изменить внешний вид сайтов Google, Facebook, YouTube, Orkut и множества других веб-страниц.",
"description": "Extension description"
},
"confirmClose": {
"message": "Закрыть",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -12,7 +12,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Ange ett namn.",
"message": "Ange ett namn",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -8,7 +8,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "Bir ad girin.",
"message": "Bir ad girin",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -8,7 +8,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "请输入名称.",
"message": "请输入名称",
"description": "Error displayed when user saves without providing a name"
},
"appliesDomainOption": {

View File

@ -48,7 +48,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "输入名称",
"message": "输入名称",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {

View File

@ -31,6 +31,14 @@
"message": "導出",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"installButton": {
"message": "安裝",
"description": "Label for install button"
},
"styleMetaErrorCheckbox": {
"message": "無效的 @var 勾選框:值必須為 0 或 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"linterJSONError": {
"message": "無效的 JSON 格式",
"description": "Setting linter config with invalid JSON"
@ -52,7 +60,7 @@
"description": "Label for the button to enable a style"
},
"styleMissingName": {
"message": "輸入名稱",
"message": "輸入名稱",
"description": "Error displayed when user saves without providing a name"
},
"genericHistoryLabel": {
@ -83,6 +91,10 @@
"message": "暗色瀏覽器主題",
"description": ""
},
"styleFromMozillaFormatError": {
"message": "從 Mozilla 格式匯入失敗",
"description": "Label for the import error"
},
"importAppendLabel": {
"message": "追加到樣式",
"description": "Label for the button to import a style and append to the existing sections"
@ -123,6 +135,10 @@
"message": "灰階淡出",
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"versionInvalidOlder": {
"message": "版本舊於已安裝的樣式。",
"description": "Displayed when the version of style is older than the installed one"
},
"confirmYes": {
"message": "是",
"description": "'Yes' button in a confirm dialog"
@ -175,10 +191,18 @@
"message": "刪除",
"description": ""
},
"confirmDefault": {
"message": "使用預設值",
"description": "'Set to default' button in a confirm dialog"
},
"confirmCancel": {
"message": "取消",
"description": ""
},
"cm_autoCloseBracketsTooltip": {
"message": "當輸入開放的 ()[]{}''\"\" 時自動新增另一邊的標點符號",
"description": "Label for the checkbox in the style editor."
},
"retrieveBckp": {
"message": "匯入樣式",
"description": ""
@ -236,6 +260,15 @@
"message": "僅本機建立的樣式",
"description": "Checkbox to show only locally created styles i.e. non-updatable"
},
"styleMetaErrorPreprocessor": {
"message": "不支援的 @preprocessor$preprocessor$",
"description": "Error displayed when the value of @preprocessor is not supported",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"linterIssuesHelp": {
"message": "這些問題被 $link$ 找到了:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
@ -245,6 +278,10 @@
}
}
},
"parseUsercssError": {
"message": "Stylus 解析 usercss 失敗:",
"description": "The error message to show when stylus failed to parse usercss"
},
"searchStyles": {
"message": "搜索內容",
"description": "Label for the search filter textbox on the Manage styles page"
@ -257,6 +294,10 @@
"message": "再次檢查,我沒有編輯任何樣式!",
"description": "Label for the button to apply all detected updates"
},
"liveReloadLabel": {
"message": "即時重新整理",
"description": "The label of live-reload feature"
},
"unreachableFileHint": {
"message": "Stylus 僅在您於 chrome://extensions 啟用了 Stylus 擴充套件中對應的勾選框時才能存取 file:// 的 URL。",
"description": "Note in the toolbar popup for file:// URLs"
@ -329,6 +370,10 @@
"message": "正規表示式無效。",
"description": "Validation message for a bad regexp in a style"
},
"license": {
"message": "授權條款",
"description": "Label for the license"
},
"optionsHeading": {
"message": "選項",
"description": "Heading for options section on manage page."
@ -371,6 +416,10 @@
"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"
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "以 usercss 編寫新樣式",
"description": ""
},
"genericResetLabel": {
"message": "重設",
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
@ -400,6 +449,10 @@
"message": "使用/re/句法正則表達式搜索",
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"popupBordersTooltip": {
"message": "對新 Chrome 中的暗色主題很有用,因為其不再繪製邊框",
"description": ""
},
"updateCheckManualUpdateHint": {
"message": "強制更新覆蓋任何本機編輯。",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
@ -412,6 +465,10 @@
"message": "棄用當前樣式內容并用導入樣式覆蓋",
"description": "Label for the button to import and overwrite current style"
},
"installUpdateFromLabel": {
"message": "檢查更新",
"description": "Label for the checkbox to save current URL for update check"
},
"cm_resizeGripHint": {
"message": "雙擊以最大化/復原高度",
"description": "Tooltip for the resize grip in style editor"
@ -444,6 +501,10 @@
"message": "Mozilla格式樣式表",
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
},
"cm_colorpicker": {
"message": "用於 CSS 色彩的顏色挑選器",
"description": "Label for the checkbox controlling colorpicker option for the style editor."
},
"writeStyleFor": {
"message": "編寫樣式給:",
"description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -545,6 +606,10 @@
"message": "禁用所有樣式",
"description": "Label for the checkbox that turns all enabled styles off."
},
"appliesLineWidgetLabel": {
"message": "顯示「套用至」資訊",
"description": "Label for the checkbox to display applies-to information in the single editor"
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "這個樣式可能在本機被編輯過。",
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
@ -585,6 +650,21 @@
"message": "Stylus 使用外部服務 https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"styleInstallOverwrite": {
"message": "「$stylename$」已安裝。要覆寫嗎?\n版本$oldVersion$→$newVersion$",
"description": "Confirmation when re-installing a style",
"placeholders": {
"stylename": {
"content": "$1"
},
"newVersion": {
"content": "$3"
},
"oldVersion": {
"content": "$2"
}
}
},
"updateCheckSkippedLocallyEdited": {
"message": "這個樣式已在本機編輯。",
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
@ -593,6 +673,10 @@
"message": "見完整的規則清單",
"description": "Stylelint or CSSLint rules label added immediately before a link"
},
"styleUpdateDiscardChanges": {
"message": "樣式已在編輯器外變更。您想要重新載入樣式嗎?",
"description": "Confirmation to update the style in the editor"
},
"optionsResetButton": {
"message": "重設選項",
"description": ""
@ -601,6 +685,10 @@
"message": "代碼",
"description": "Label for the code for a section"
},
"externalUsercssDocument": {
"message": "Usercss 的文件",
"description": "Label for the external link to usercss documentation"
},
"optionsAdvancedContextDelete": {
"message": "在編輯器的右鍵選單中加入「刪除」",
"description": ""
@ -614,6 +702,10 @@
}
}
},
"configureStyle": {
"message": "設定",
"description": "Label for the button to configure userstyle"
},
"importReportLegendUpdatedBoth": {
"message": "後設資訊與程式碼均已更新",
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
@ -642,6 +734,32 @@
"message": "在輸入時自動完成",
"description": "Label for the checkbox in the style editor."
},
"linterCSSLintIncompatible": {
"message": "CSSLint 不支援 $preprocessorname$ 預處理器",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"styleMetaErrorSelectValueMismatch": {
"message": "無效的 @select值不存在於清單中",
"description": "Error displayed when the value of @select is invalid"
},
"styleMetaErrorColor": {
"message": "$color$是無效的顏色",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"externalFeedback": {
"message": "回饋",
"description": "Label for the external link to send feedback for the style"
},
"manageOnlyDisabled": {
"message": "僅已停用的樣式",
"description": "Checkbox to show only disabled styles"
@ -666,6 +784,10 @@
"message": "Stylus 不能在諸如此類的網頁上生效。",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
},
"popupBorders": {
"message": "在邊邊新增白色邊框",
"description": ""
},
"manageOnlyUpdates": {
"message": "僅有更新或是有問題的",
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
@ -674,6 +796,14 @@
"message": "添加樣式",
"description": "Title of the page for adding styles"
},
"externalLink": {
"message": "外部連結",
"description": "Label for external links"
},
"externalHomepage": {
"message": "首頁",
"description": "Label for the external link to style's homepage"
},
"importReplaceLabel": {
"message": "覆蓋樣式",
"description": "Label for the button to import and overwrite current style"
@ -690,6 +820,10 @@
"message": "進階",
"description": ""
},
"alphaChannel": {
"message": "不透明度",
"description": "Label of color's opacity"
},
"importAppendTooltip": {
"message": "追加導入的樣式到當前樣式",
"description": "Tooltip for the button to import a style and append to the existing sections"
@ -742,10 +876,23 @@
"message": "選項介面",
"description": "Go to Options UI"
},
"colorpickerTooltip": {
"message": "開啟顏色挑選器",
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
},
"optionsCustomizeBadge": {
"message": "在工具列圖示上的徽章",
"description": ""
},
"installUpdateFrom": {
"message": "目前從 $url$ 更新樣式",
"description": "Label to describe where the style gets update",
"placeholders": {
"url": {
"content": "$1"
}
}
},
"importReportLegendIdentical": {
"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"
@ -754,6 +901,14 @@
"message": "彈出視窗寬度(以像素計)",
"description": ""
},
"cm_autoCloseBrackets": {
"message": "自動關閉括號與引號",
"description": "Label for the checkbox in the style editor."
},
"installButtonReinstall": {
"message": "重新安裝",
"description": "Label for reinstall button"
},
"linterInvalidConfigError": {
"message": "因為無效的設定所以未儲存:",
"description": "Invalid linter config will show a message followed by a list of invalid entries"
@ -762,6 +917,19 @@
"message": "否",
"description": "'No' button in a confirm dialog"
},
"styleMissingMeta": {
"message": "遺失詮釋資料 @ $key$",
"description": "Error displayed when a mandatory metadata is missing",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"appliesLineWidgetWarning": {
"message": "無法與最小化的 CSS 一起運作",
"description": "A warning that applies-to information won't show properly with minified CSS"
},
"undo": {
"message": "撤銷",
"description": "Button label"
@ -770,6 +938,14 @@
"message": "鍵盤映射",
"description": "Label for the drop-down list controlling the keymap for the style editor."
},
"externalSupport": {
"message": "支援",
"description": "Label for the external link to style's support site"
},
"confirmSave": {
"message": "儲存",
"description": "'Save' button in a confirm dialog"
},
"manageNewUI": {
"message": "新的管理介面佈局",
"description": "Label for the checkbox that toggles the new UI on manage page"
@ -790,6 +966,10 @@
"message": "替換為",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"liveReloadError": {
"message": "觀看檔案時發生錯誤",
"description": "The label of live-reload error"
},
"deleteStyleLabel": {
"message": "删除",
"description": "Label for the button to delete a style"
@ -798,6 +978,15 @@
"message": "安裝更新(本機編輯將會被覆寫)",
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
},
"styleInstallFailed": {
"message": "安裝使用者樣式失敗!\n$error$",
"description": "Warning when installation failed",
"placeholders": {
"error": {
"content": "$1"
}
}
},
"optionsAdvancedExposeIframes": {
"message": "透過 HTML[stylus-iframe] 公開 iframes",
"description": ""
@ -810,6 +999,10 @@
"message": "要停用自動化使用者樣式更新檢查,設定間隔為 0",
"description": ""
},
"installButtonUpdate": {
"message": "更新",
"description": "Label for update button"
},
"backupButtons": {
"message": "備份",
"description": "Heading for backup"
@ -822,6 +1015,14 @@
"message": "編輯",
"description": "Label for the button to go to the edit style page"
},
"installButtonInstalled": {
"message": "已安裝",
"description": "Text displayed when the style is successfully installed"
},
"author": {
"message": "作者",
"description": "Label for the style author"
},
"cm_theme": {
"message": "主題",
"description": "Label for the style editor's CSS theme."
@ -830,6 +1031,10 @@
"message": "在新視窗開啟編輯器",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"appliesRemoveError": {
"message": "無法移除最後的「套用到」項目",
"description": "Error displayed when the last 'applies' is going to be removed"
},
"backupMessage": {
"message": "選取檔案並拖曳到此頁面。",
"description": "Message for backup"
@ -849,5 +1054,9 @@
"description": {
"message": "用Stylus一個用戶樣式管理器重塑網頁。 Stylus 讓你能為诸多主流網站輕鬆的安裝主題和皮膚。",
"description": "Extension description"
},
"confirmClose": {
"message": "關閉",
"description": "'Close' button in a confirm dialog"
}
}

View File

@ -28,16 +28,33 @@ chrome.runtime.onMessage.addListener(onRuntimeMessage);
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
listener('styleReplaceAll', data));
if (FIREFOX) {
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
chrome.webNavigation.onCommitted.addListener(webNavUsercssInstallerFF, {
url: [
{urlPrefix: 'https://raw.githubusercontent.com/', urlSuffix: '.user.css'},
{urlPrefix: 'https://raw.githubusercontent.com/', urlSuffix: '.user.styl'},
]
});
}
}
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
if (chrome.contextMenus) {
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
}
if ('commands' in chrome) {
if (chrome.commands) {
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
chrome.commands.onCommand.addListener(command => browserCommands[command]());
}
if (!chrome.browserAction ||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
window.updateIcon = () => {};
}
// *************************************************************************
// set the default icon displayed after a tab is created until webNavigation kicks in
prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {}));
@ -50,9 +67,13 @@ prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {}));
// Open FAQs page once after installation to guide new users.
// Do not display it in development mode.
if (reason === 'install' && manifest.update_url) {
setTimeout(openURL, 100, {
url: 'http://add0n.com/stylus.html'
});
// don't hardcode homepage URL, extract it from "Get Help" label translation
// TODO: add a built-in tour page in the extension
const getHelpHtml = chrome.i18n.getMessage('manageText').match(/<a\s+href=[^>]+/g);
const url = (getHelpHtml[1] || '').replace(/^.+?=\s*/, '').replace(/^['"]|["']$/g, '');
if (url) {
setTimeout(openURL, 100, {url});
}
}
// reset L10N cache on update
if (reason === 'update') {
@ -79,7 +100,7 @@ browserCommands = {
// *************************************************************************
// context menus
contextMenus = Object.assign({
contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
@ -92,22 +113,27 @@ contextMenus = Object.assign({
title: 'openStylesManager',
click: browserCommands.openManage,
},
}, !FIREFOX && prefs.get('editor.contextDelete') && {
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'});
sendMessage({tabId: tab.id, method: 'editDeleteText'});
},
}
});
};
{
const createContextMenus = (ids = Object.keys(contextMenus)) => {
if (chrome.contextMenus) {
const createContextMenus = ids => {
for (const id of ids) {
const item = Object.assign({id}, contextMenus[id]);
let item = contextMenus[id];
if (item.presentIf && !item.presentIf()) {
continue;
}
item = Object.assign({id}, item);
delete item.presentIf;
const prefValue = prefs.readOnlyValues[id];
item.title = chrome.i18n.getMessage(item.title);
if (!item.type && typeof prefValue === 'boolean') {
@ -121,20 +147,20 @@ contextMenus = Object.assign({
chrome.contextMenus.create(item, ignoreChromeError);
}
};
createContextMenus();
const toggleableIds = Object.keys(contextMenus).filter(key =>
typeof prefs.readOnlyValues[key] === 'boolean');
prefs.subscribe(toggleableIds, (id, checked) => {
if (id === 'editor.contextDelete') {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
const toggleCheckmark = (id, checked) => {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
};
const togglePresence = (id, checked) => {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
chrome.contextMenus.remove(id, ignoreChromeError);
}
});
};
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.readOnlyValues[id] === 'boolean'), toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
createContextMenus(keys);
}
// *************************************************************************
@ -145,7 +171,6 @@ window.addEventListener('storageReady', function _() {
updateIcon({id: undefined}, {});
const NTP = 'chrome://newtab/';
const PING = {method: 'ping'};
const ALL_URLS = '<all_urls>';
const contentScripts = chrome.runtime.getManifest().content_scripts;
// expand * as .*?
@ -168,15 +193,11 @@ window.addEventListener('storageReady', function _() {
};
const pingCS = (cs, {id, url}) => {
const maybeInject = pong => !pong && injectCS(cs, id);
cs.matches.some(match => {
if ((match === ALL_URLS || url.match(match))
&& (!url.startsWith('chrome') || url === NTP)) {
chrome.tabs.sendMessage(id, PING, pong => {
if (!pong) {
injectCS(cs, id);
}
ignoreChromeError();
});
if ((match === ALL_URLS || url.match(match)) &&
(!url.startsWith('chrome') || url === NTP)) {
sendMessage({method: 'ping', tabId: id}, maybeInject);
return true;
}
});
@ -200,13 +221,13 @@ function webNavigationListener(method, {url, tabId, frameId}) {
if (method === 'styleApply') {
handleCssTransitionBug({tabId, frameId, url, styles});
}
chrome.tabs.sendMessage(tabId, {
sendMessage({
tabId,
frameId,
method,
// ping own page so it retrieves the styles directly
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
}, {
frameId
}, ignoreChromeError);
});
}
// main page frame id is 0
if (frameId === 0) {
@ -234,6 +255,21 @@ function webNavigationListenerChrome(method, data) {
}
function webNavUsercssInstallerFF(data) {
const {tabId} = data;
Promise.all([
sendMessage({tabId, method: 'ping'}),
// we need tab index to open the installer next to the original one
// and also to skip the double-invocation in FF which assigns tab url later
getTab(tabId),
]).then(([pong, tab]) => {
if (pong !== true && tab.url !== 'about:blank') {
usercssHelper.openInstallPage(tab, {direct: true});
}
});
}
function updateIcon(tab, styles) {
if (tab.id < 0) {
return;
@ -281,20 +317,28 @@ function updateIcon(tab, styles) {
}
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
chrome.browserAction.setBadgeBackgroundColor({color});
getTab(tab.id).then(realTab => {
// skip pre-rendered tabs
if (realTab.index >= 0) {
chrome.browserAction.setBadgeText({text, tabId: tab.id});
}
setTimeout(() => {
getTab(tab.id).then(realTab => {
// skip pre-rendered tabs
if (realTab.index >= 0) {
chrome.browserAction.setBadgeText({text, tabId: tab.id});
}
});
});
});
}
}
function onRuntimeMessage(request, sender, sendResponse) {
// prevent browser exception bug on sending a response to a closed tab
sendResponse = (send => data => tryCatch(send, data))(sendResponse);
function onRuntimeMessage(request, sender, sendResponseInternal) {
const sendResponse = data => {
// wrap Error object instance as {__ERROR__: message} - will be unwrapped in sendMessage
if (data instanceof Error) {
data = {__ERROR__: data.message};
}
// prevent browser exception bug on sending a response to a closed tab
tryCatch(sendResponseInternal, data);
};
switch (request.method) {
case 'getStyles':
getStyles(request).then(sendResponse);
@ -333,7 +377,11 @@ function onRuntimeMessage(request, sender, sendResponse) {
return KEEP_CHANNEL_OPEN;
case 'closeTab':
closeTab(sender.tab.id, request).then(sendResponse);
chrome.tabs.remove(request.tabId || sender.tab.id, () => {
if (chrome.runtime.lastError && request.tabId !== sender.tab.id) {
sendResponse(new Error(chrome.runtime.lastError.message));
}
});
return KEEP_CHANNEL_OPEN;
case 'openEditor':
@ -341,22 +389,3 @@ function onRuntimeMessage(request, sender, sendResponse) {
return;
}
}
function closeTab(tabId, request) {
return new Promise(resolve => {
if (request.tabId) {
tabId = request.tabId;
}
chrome.tabs.remove(tabId, () => {
const {lastError} = chrome.runtime;
if (lastError) {
resolve({
success: false,
error: lastError.message || String(lastError)
});
return;
}
resolve({success: true});
});
});
}

View File

@ -203,12 +203,18 @@ function dbExecChromeStorage(method, data) {
case 'getAll':
return chromeLocal.get(null).then(storage => {
const styles = [];
const leftovers = [];
for (const key in storage) {
if (key.startsWith(STYLE_KEY_PREFIX) &&
Number(key.substr(STYLE_KEY_PREFIX.length))) {
styles.push(storage[key]);
} else if (key.startsWith('tempUsercssCode')) {
leftovers.push(key);
}
}
if (leftovers.length) {
chromeLocal.remove(leftovers);
}
return {target: {result: styles}};
});
}
@ -389,7 +395,7 @@ function saveStyle(style) {
.then(decide);
function maybeCalcDigest() {
if (reason === 'update' || reason === 'update-digest') {
if (['install', 'update', 'update-digest'].includes(reason)) {
return calcStyleDigest(style).then(digest => {
style.originalDigest = digest;
});
@ -418,7 +424,7 @@ function saveStyle(style) {
return style;
}
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
style = Object.assign({}, oldStyle, style);
style = Object.assign({installDate: Date.now()}, oldStyle, style);
return write(style, store);
});
} else {

View File

@ -127,6 +127,7 @@ var updater = {
function maybeSave(json) {
json.id = style.id;
json.updateDate = Date.now();
if (styleSectionsEqual(json, style)) {
// JSONs may have different order of items even if sections are effectively equal
// so we'll update the digest anyway

View File

@ -1,4 +1,4 @@
/* global usercss saveStyle getStyles */
/* global usercss saveStyle getStyles chromeLocal */
'use strict';
// eslint-disable-next-line no-var
@ -22,8 +22,8 @@ var usercssHelper = (() => {
}
function wrapReject(pending) {
return pending.then(result => ({success: true, result}))
.catch(err => ({success: false, result: err.message || String(err)}));
return pending
.catch(err => new Error(Array.isArray(err) ? err.join('\n') : err.message || String(err)));
}
// Parse the source and find the duplication
@ -78,16 +78,30 @@ var usercssHelper = (() => {
);
}
function openInstallPage(tab, request) {
const url = '/install-usercss.html' +
'?updateUrl=' + encodeURIComponent(request.updateUrl) +
'&tabId=' + tab.id;
function openInstallPage(tab, {url = tab.url, direct} = {}) {
if (direct) {
prefetchCodeForInstallation(tab.id, url);
}
return wrapReject(openURL({
url,
url: '/install-usercss.html' +
'?updateUrl=' + encodeURIComponent(url) +
'&tabId=' + tab.id +
(direct ? '&direct=yes' : ''),
index: tab.index + 1,
openerTabId: tab.id,
}));
}
function prefetchCodeForInstallation(tabId, url) {
const key = 'tempUsercssCode' + tabId;
Promise.all([
download(url),
chromeLocal.setValue(key, {loading: true}),
]).then(([code]) => {
chromeLocal.setValue(key, code);
setTimeout(() => chromeLocal.remove(key), 60e3);
});
}
return {build, save, findDup, openInstallPage};
})();

View File

@ -1,398 +1,498 @@
/* eslint no-var: 0 */
'use strict';
var ID_PREFIX = 'stylus-';
var ROOT = document.documentElement;
var isOwnPage = location.protocol.endsWith('-extension:');
var disableAll = false;
var exposeIframes = false;
var styleElements = new Map();
var disabledElements = new Map();
var retiredStyleTimers = new Map();
var docRewriteObserver;
var docRootObserver;
(() => {
var ID_PREFIX = 'stylus-';
var ROOT = document.documentElement;
var isOwnPage = location.protocol.endsWith('-extension:');
var disableAll = false;
var exposeIframes = false;
var styleElements = new Map();
var disabledElements = new Map();
var retiredStyleTimers = new Map();
var docRewriteObserver;
var docRootObserver;
requestStyles();
chrome.runtime.onMessage.addListener(applyOnMessage);
requestStyles();
chrome.runtime.onMessage.addListener(applyOnMessage);
window.applyOnMessage = applyOnMessage;
if (!isOwnPage) {
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
window.addEventListener(chrome.runtime.id, orphanCheck, true);
}
function requestStyles(options, callback = applyStyles) {
if (!chrome.app && document instanceof XMLDocument) {
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
return;
}
var matchUrl = location.href;
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
// dynamic about: and javascript: iframes don't have an URL yet
// so we'll try the parent frame which is guaranteed to have a real URL
try {
if (window !== parent) {
matchUrl = parent.location.href;
}
} catch (e) {}
}
const request = Object.assign({
method: 'getStyles',
matchUrl,
enabled: true,
asHash: true,
}, options);
// On own pages we request the styles directly to minimize delay and flicker
if (typeof getStylesSafe === 'function') {
getStylesSafe(request).then(callback);
} else {
chrome.runtime.sendMessage(request, callback);
}
}
function applyOnMessage(request, sender, sendResponse) {
if (request.styles === 'DIY') {
// Do-It-Yourself tells our built-in pages to fetch the styles directly
// which is faster because IPC messaging JSON-ifies everything internally
requestStyles({}, styles => {
request.styles = styles;
applyOnMessage(request);
});
return;
if (!isOwnPage) {
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
window.addEventListener(chrome.runtime.id, orphanCheck, true);
}
if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
request.action = request.method;
request.method = 'styleViaAPI';
request.styles = null;
if (request.style) {
request.style.sections = null;
function requestStyles(options, callback = applyStyles) {
if (!chrome.app && document instanceof XMLDocument) {
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
return;
}
var matchUrl = location.href;
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
// dynamic about: and javascript: iframes don't have an URL yet
// so we'll try the parent frame which is guaranteed to have a real URL
try {
if (window !== parent) {
matchUrl = parent.location.href;
}
} catch (e) {}
}
const request = Object.assign({
method: 'getStyles',
matchUrl,
enabled: true,
asHash: true,
}, options);
// On own pages we request the styles directly to minimize delay and flicker
if (typeof getStylesSafe === 'function') {
getStylesSafe(request).then(callback);
} else {
chrome.runtime.sendMessage(request, callback);
}
chrome.runtime.sendMessage(request);
return;
}
switch (request.method) {
case 'styleDeleted':
removeStyle(request);
break;
case 'styleUpdated':
if (request.codeIsUpdated === false) {
applyStyleState(request.style);
function applyOnMessage(request, sender, sendResponse) {
if (request.styles === 'DIY') {
// Do-It-Yourself tells our built-in pages to fetch the styles directly
// which is faster because IPC messaging JSON-ifies everything internally
requestStyles({}, styles => {
request.styles = styles;
applyOnMessage(request);
});
return;
}
if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
request.action = request.method;
request.method = 'styleViaAPI';
request.styles = null;
if (request.style) {
request.style.sections = null;
}
chrome.runtime.sendMessage(request);
return;
}
switch (request.method) {
case 'styleDeleted':
removeStyle(request);
break;
case 'styleUpdated':
if (request.codeIsUpdated === false) {
applyStyleState(request.style);
break;
}
if (request.style.enabled) {
removeStyle({id: request.style.id, retire: true});
requestStyles({id: request.style.id});
} else {
removeStyle(request.style);
}
break;
case 'styleAdded':
if (request.style.enabled) {
requestStyles({id: request.style.id});
}
break;
case 'styleApply':
applyStyles(request.styles);
break;
case 'styleReplaceAll':
replaceAll(request.styles);
break;
case 'prefChanged':
if ('disableAll' in request.prefs) {
doDisableAll(request.prefs.disableAll);
}
if ('exposeIframes' in request.prefs) {
doExposeIframes(request.prefs.exposeIframes);
}
break;
case 'ping':
sendResponse(true);
break;
}
}
function doDisableAll(disable = disableAll) {
if (!disable === !disableAll) {
return;
}
disableAll = disable;
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
&& stylesheet.disabled !== disable) {
stylesheet.disabled = disable;
}
});
}
function doExposeIframes(state = exposeIframes) {
if (state === exposeIframes || window === parent) {
return;
}
exposeIframes = state;
const attr = document.documentElement.getAttribute('stylus-iframe');
if (state && attr !== '') {
document.documentElement.setAttribute('stylus-iframe', '');
} else if (!state && attr === '') {
document.documentElement.removeAttribute('stylus-iframe');
}
}
function applyStyleState({id, enabled}) {
const inCache = disabledElements.get(id) || styleElements.get(id);
const inDoc = document.getElementById(ID_PREFIX + id);
if (enabled) {
if (inDoc) {
return;
} else if (inCache) {
addStyleElement(inCache);
disabledElements.delete(id);
} else {
requestStyles({id});
}
} else {
if (inDoc) {
disabledElements.set(id, inDoc);
docRootObserver.stop();
inDoc.remove();
docRootObserver.start();
}
}
}
function removeStyle({id, retire = false}) {
const el = document.getElementById(ID_PREFIX + id);
if (el) {
if (retire) {
// to avoid page flicker when the style is updated
// instead of removing it immediately we rename its ID and queue it
// to be deleted in applyStyles after a new version is fetched and applied
const deadID = 'ghost-' + id;
el.id = ID_PREFIX + deadID;
// in case something went wrong and new style was never applied
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
} else {
el.remove();
}
}
styleElements.delete(ID_PREFIX + id);
disabledElements.delete(id);
retiredStyleTimers.delete(id);
}
function applyStyles(styles) {
if (!styles) {
// Chrome is starting up
requestStyles();
return;
}
if ('disableAll' in styles) {
doDisableAll(styles.disableAll);
delete styles.disableAll;
}
if ('exposeIframes' in styles) {
doExposeIframes(styles.exposeIframes);
delete styles.exposeIframes;
}
const gotNewStyles = Object.keys(styles).length || styles.needTransitionPatch;
if (gotNewStyles) {
if (docRootObserver) {
docRootObserver.stop();
} else {
initDocRootObserver();
}
}
if (styles.needTransitionPatch) {
// CSS transition bug workaround: since we insert styles asynchronously,
// the browsers, especially Firefox, may apply all transitions on page load
delete styles.needTransitionPatch;
const className = chrome.runtime.id + '-transition-bug-fix';
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
document.documentElement.classList.add(className);
applySections(0, `
${docId}.${className}:root * {
transition: none !important;
}
`);
setTimeout(() => {
removeStyle({id: 0});
document.documentElement.classList.remove(className);
});
}
if (gotNewStyles) {
for (const id in styles) {
applySections(id, styles[id].map(section => section.code).join('\n'));
}
docRootObserver.start({sort: true});
}
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
initDocRewriteObserver();
}
if (retiredStyleTimers.size) {
setTimeout(() => {
for (const [id, timer] of retiredStyleTimers.entries()) {
removeStyle({id});
clearTimeout(timer);
}
});
}
}
function applySections(styleId, code) {
const id = ID_PREFIX + styleId;
let el = styleElements.get(id) || document.getElementById(id);
if (!el) {
if (document.documentElement instanceof SVGSVGElement) {
// SVG document style
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
} else if (document instanceof XMLDocument) {
// XML document style
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
} else {
// HTML document style; also works on HTML-embedded SVG
el = document.createElement('style');
}
Object.assign(el, {
id,
type: 'text/css',
textContent: code,
});
// SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus');
addStyleElement(el);
}
styleElements.set(id, el);
disabledElements.delete(Number(styleId));
return el;
}
function addStyleElement(newElement) {
if (!ROOT) {
return;
}
let next;
const newStyleId = getStyleId(newElement);
for (const el of styleElements.values()) {
if (el.parentNode && !el.id.endsWith('-ghost') && getStyleId(el) > newStyleId) {
next = el.parentNode === ROOT ? el : null;
break;
}
if (request.style.enabled) {
removeStyle({id: request.style.id, retire: true});
requestStyles({id: request.style.id});
} else {
removeStyle(request.style);
}
break;
case 'styleAdded':
if (request.style.enabled) {
requestStyles({id: request.style.id});
}
break;
case 'styleApply':
applyStyles(request.styles);
break;
case 'styleReplaceAll':
replaceAll(request.styles);
break;
case 'prefChanged':
if ('disableAll' in request.prefs) {
doDisableAll(request.prefs.disableAll);
}
if ('exposeIframes' in request.prefs) {
doExposeIframes(request.prefs.exposeIframes);
}
break;
case 'ping':
sendResponse(true);
break;
}
}
function doDisableAll(disable = disableAll) {
if (!disable === !disableAll) {
return;
}
disableAll = disable;
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
&& stylesheet.disabled !== disable) {
stylesheet.disabled = disable;
}
});
}
function doExposeIframes(state = exposeIframes) {
if (state === exposeIframes || window === parent) {
return;
}
exposeIframes = state;
const attr = document.documentElement.getAttribute('stylus-iframe');
if (state && attr !== '') {
document.documentElement.setAttribute('stylus-iframe', '');
} else if (!state && attr === '') {
document.documentElement.removeAttribute('stylus-iframe');
}
}
function applyStyleState({id, enabled}) {
const inCache = disabledElements.get(id) || styleElements.get(id);
const inDoc = document.getElementById(ID_PREFIX + id);
if (enabled) {
if (inDoc) {
if (next === newElement.nextElementSibling) {
return;
} else if (inCache) {
addStyleElement(inCache);
disabledElements.delete(id);
} else {
requestStyles({id});
}
} else {
if (inDoc) {
disabledElements.set(id, inDoc);
inDoc.remove();
}
}
}
function removeStyle({id, retire = false}) {
const el = document.getElementById(ID_PREFIX + id);
if (el) {
if (retire) {
// to avoid page flicker when the style is updated
// instead of removing it immediately we rename its ID and queue it
// to be deleted in applyStyles after a new version is fetched and applied
const deadID = 'ghost-' + id;
el.id = ID_PREFIX + deadID;
// in case something went wrong and new style was never applied
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
} else {
el.remove();
}
}
styleElements.delete(ID_PREFIX + id);
disabledElements.delete(id);
retiredStyleTimers.delete(id);
}
function applyStyles(styles) {
if (!styles) {
// Chrome is starting up
requestStyles();
return;
}
if ('disableAll' in styles) {
doDisableAll(styles.disableAll);
delete styles.disableAll;
}
if ('exposeIframes' in styles) {
doExposeIframes(styles.exposeIframes);
delete styles.exposeIframes;
}
if (styles.needTransitionPatch) {
// CSS transition bug workaround: since we insert styles asynchronously,
// the browsers, especially Firefox, may apply all transitions on page load
delete styles.needTransitionPatch;
const className = chrome.runtime.id + '-transition-bug-fix';
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
document.documentElement.classList.add(className);
applySections(0, `
${docId}.${className}:root * {
transition: none !important;
}
`);
setTimeout(() => {
removeStyle({id: 0});
document.documentElement.classList.remove(className);
});
}
for (const id in styles) {
applySections(id, styles[id].map(section => section.code).join('\n'));
}
initDocRewriteObserver();
initDocRootObserver();
if (retiredStyleTimers.size) {
setTimeout(() => {
for (const [id, timer] of retiredStyleTimers.entries()) {
removeStyle({id});
clearTimeout(timer);
}
});
}
}
function applySections(styleId, code) {
let el = document.getElementById(ID_PREFIX + styleId);
if (el) {
return;
}
if (document.documentElement instanceof SVGSVGElement) {
// SVG document style
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
} else if (document instanceof XMLDocument) {
// XML document style
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
} else {
// HTML document style; also works on HTML-embedded SVG
el = document.createElement('style');
}
Object.assign(el, {
id: ID_PREFIX + styleId,
type: 'text/css',
textContent: code,
});
// SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus');
addStyleElement(el);
styleElements.set(el.id, el);
disabledElements.delete(Number(styleId));
return el;
}
function addStyleElement(el) {
if (ROOT && !document.getElementById(el.id)) {
ROOT.appendChild(el);
docRootObserver.stop();
ROOT.insertBefore(newElement, next || null);
if (disableAll) {
el.disabled = true;
newElement.disabled = true;
}
docRootObserver.start();
}
}
function replaceAll(newStyles) {
const oldStyles = Array.prototype.slice.call(
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
oldStyles.forEach(el => (el.id += '-ghost'));
styleElements.clear();
disabledElements.clear();
[...retiredStyleTimers.values()].forEach(clearTimeout);
retiredStyleTimers.clear();
applyStyles(newStyles);
oldStyles.forEach(el => el.remove());
}
function initDocRewriteObserver() {
if (isOwnPage || docRewriteObserver || !styleElements.size) {
return;
function replaceAll(newStyles) {
const oldStyles = Array.prototype.slice.call(
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
oldStyles.forEach(el => (el.id += '-ghost'));
styleElements.clear();
disabledElements.clear();
[...retiredStyleTimers.values()].forEach(clearTimeout);
retiredStyleTimers.clear();
applyStyles(newStyles);
oldStyles.forEach(el => el.remove());
}
// re-add styles if we detect documentElement being recreated
const reinjectStyles = () => {
if (!styleElements) {
return orphanCheck && orphanCheck();
function getStyleId(el) {
return parseInt(el.id.substr(ID_PREFIX.length));
}
function orphanCheck() {
if (chrome.i18n && chrome.i18n.getUILanguage()) {
return true;
}
ROOT = document.documentElement;
for (const el of styleElements.values()) {
el.textContent += ' '; // invalidate CSSOM cache
addStyleElement(document.importNode(el, true));
}
};
// detect documentElement being rewritten from inside the script
docRewriteObserver = new MutationObserver(mutations => {
for (let m = mutations.length; --m >= 0;) {
const added = mutations[m].addedNodes;
for (let n = added.length; --n >= 0;) {
if (added[n].localName === 'html') {
reinjectStyles();
return;
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.takeRecords() && ob.disconnect());
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
}
function initDocRewriteObserver() {
// detect documentElement being rewritten from inside the script
docRewriteObserver = new MutationObserver(mutations => {
for (let m = mutations.length; --m >= 0;) {
const added = mutations[m].addedNodes;
for (let n = added.length; --n >= 0;) {
if (added[n].localName === 'html') {
reinjectStyles();
return;
}
}
}
}
});
docRewriteObserver.observe(document, {childList: true});
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
setTimeout(() => {
if (document.documentElement !== ROOT) {
reinjectStyles();
}
});
}
function initDocRootObserver() {
if (!styleElements.size || document.body || docRootObserver) {
return;
}
// wait for BODY and move all style elements after it
docRootObserver = new MutationObserver(() => {
let expectedPrevSibling = document.body || document.head;
if (!expectedPrevSibling) {
return;
}
docRootObserver.disconnect();
for (const el of styleElements.values()) {
if (el.previousElementSibling !== expectedPrevSibling) {
ROOT.insertBefore(el, expectedPrevSibling.nextSibling);
if (el.disabled !== disableAll) {
// moving an element resets its 'disabled' state
el.disabled = disableAll;
}
});
docRewriteObserver.observe(document, {childList: true});
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
setTimeout(() => {
if (document.documentElement !== ROOT) {
reinjectStyles();
}
expectedPrevSibling = el;
});
// re-add styles if we detect documentElement being recreated
function reinjectStyles() {
if (!styleElements) {
orphanCheck();
return;
}
ROOT = document.documentElement;
docRootObserver.stop();
const imported = [];
for (const [id, el] of styleElements.entries()) {
const copy = document.importNode(el, true);
el.textContent += ' '; // invalidate CSSOM cache
imported.push([id, copy]);
addStyleElement(copy);
}
docRootObserver.start();
styleElements = new Map(imported);
}
if (document.body) {
docRootObserver = null;
} else {
docRootObserver.connect();
}
});
docRootObserver.connect = () => {
docRootObserver.observe(ROOT, {childList: true});
};
docRootObserver.connect();
}
function orphanCheck() {
const port = chrome.runtime.connect();
if (port) {
port.disconnect();
return;
}
// we're orphaned due to an extension update
// we can detach the mutation observer
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
// we can detach event listeners
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
// we can destroy our globals in this context to free up memory
[ // functions
'addStyleElement',
'applyOnMessage',
'applySections',
'applyStyles',
'applyStyleState',
'doDisableAll',
'initDocRewriteObserver',
'initDocRootObserver',
'orphanCheck',
'removeStyle',
'replaceAll',
'requestStyles',
// variables
'ROOT',
'disabledElements',
'retiredStyleTimers',
'styleElements',
'docRewriteObserver',
'docRootObserver',
].forEach(fn => (window[fn] = null));
}
function initDocRootObserver() {
let lastRestorationTime = 0;
let restorationCounter = 0;
let observing = false;
let sorting = false;
// allow any types of elements between ours, except for the following:
const ORDERED_TAGS = ['head', 'body', 'style', 'link'];
init();
return;
function init() {
docRootObserver = new MutationObserver(sortStyleElements);
Object.assign(docRootObserver, {start, stop});
}
function start({sort = false} = {}) {
if (sort && sortStyleMap()) {
sortStyleElements();
}
if (!observing && ROOT) {
docRootObserver.observe(ROOT, {childList: true});
observing = true;
}
}
function stop() {
if (observing) {
docRootObserver.disconnect();
observing = false;
}
}
function sortStyleMap() {
const list = [];
let prevStyleId = 0;
let needsSorting = false;
for (const entry of styleElements.entries()) {
list.push(entry);
const el = entry[1];
const styleId = getStyleId(el);
el.styleId = styleId;
needsSorting |= styleId < prevStyleId;
prevStyleId = styleId;
}
if (needsSorting) {
styleElements = new Map(list.sort((a, b) => a[1].styleId - b[1].styleId));
return true;
}
}
function sortStyleElements() {
let expected = document.body || document.head;
if (!expected || sorting) {
return;
}
for (const el of styleElements.values()) {
if (!isMovable(el)) {
continue;
}
let prev = el.previousElementSibling;
while (prev !== expected) {
if (prev && isSkippable(prev)) {
expected = prev;
prev = prev.nextElementSibling;
} else if (!moveAfter(el, expected)) {
return;
} else {
break;
}
}
expected = el;
}
if (sorting) {
sorting = false;
docRootObserver.takeRecords();
setTimeout(start);
//docRootObserver.start();
}
}
function isMovable(el) {
return el.parentNode || !disabledElements.has(getStyleId(el));
}
function isSkippable(el) {
return !ORDERED_TAGS.includes(el.localName) ||
el.id.startsWith(ID_PREFIX) &&
el.id.endsWith('-ghost') &&
el.localName === 'style' &&
el.className === 'stylus';
}
function moveAfter(el, expected) {
if (!sorting) {
if (restorationLimitExceeded()) {
return false;
}
sorting = true;
docRootObserver.stop();
}
expected.insertAdjacentElement('afterend', el);
if (el.disabled !== disableAll) {
// moving an element resets its 'disabled' state
el.disabled = disableAll;
}
return true;
}
function restorationLimitExceeded() {
const t = performance.now();
if (t - lastRestorationTime > 1000) {
restorationCounter = 0;
}
lastRestorationTime = t;
if (++restorationCounter > 100) {
console.error('Stylus stopped restoring userstyle elements after 100 failed attempts.\n' +
'Please report on https://github.com/openstyles/stylus/issues');
return true;
}
}
}
})();

View File

@ -1,4 +1,3 @@
/* global runtimeSend */
'use strict';
function createSourceLoader() {
@ -66,10 +65,6 @@ function initUsercssInstall() {
let watcher;
chrome.runtime.onConnect.addListener(port => {
// FIXME: is this the correct way to reject a connection?
// https://developer.chrome.com/extensions/messaging#connect
console.assert(port.name === 'usercss-install');
port.onMessage.addListener(msg => {
switch (msg.method) {
case 'getSourceCode':
@ -95,16 +90,16 @@ function initUsercssInstall() {
if (history.length > 1) {
history.back();
} else {
runtimeSend({method: 'closeTab'});
chrome.runtime.sendMessage({method: 'closeTab'});
}
break;
}
});
});
return runtimeSend({
chrome.runtime.sendMessage({
method: 'openUsercssInstallPage',
updateUrl: location.href
}).catch(alert);
url: location.href,
}, r => r && r.__ERROR__ && alert(r.__ERROR__));
}
function isUsercss() {

View File

@ -0,0 +1,369 @@
'use strict';
(() => {
const FIREFOX = !chrome.app;
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
['Update', 'Install'].forEach(type =>
['', 'Chrome', 'Opera'].forEach(browser =>
document.addEventListener('stylish' + type + browser, onClick)));
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// orphaned content script check
if (msg.method === 'ping') {
sendResponse(true);
}
});
new MutationObserver((mutations, observer) => {
if (document.body) {
observer.disconnect();
// TODO: remove the following statement when USO pagination title is fixed
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
chrome.runtime.sendMessage({
method: 'getStyles',
url: getMeta('stylish-id-url') || location.href
}, checkUpdatability);
}
}).observe(document.documentElement, {childList: true});
/* since we are using "stylish-code-chrome" meta key on all browsers and
US.o does not provide "advanced settings" on this url if browser is not Chrome,
we need to fix this URL using "stylish-update-url" meta key
*/
function getStyleURL() {
const textUrl = getMeta('stylish-update-url') || '';
const jsonUrl = getMeta('stylish-code-chrome') ||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
}
function checkUpdatability([installedStyle]) {
// TODO: remove the following statement when USO is fixed
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
detail: installedStyle && installedStyle.updateUrl,
}));
if (!installedStyle) {
sendEvent('styleCanBeInstalledChrome');
return;
}
const md5Url = getMeta('stylish-md5-url');
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
getResource(md5Url).then(md5 => {
reportUpdatable(md5 !== installedStyle.originalMd5);
});
} else {
getStyleJson().then(json => {
reportUpdatable(!json ||
!styleSectionsEqual(json, installedStyle));
});
}
function reportUpdatable(isUpdatable) {
sendEvent(
isUpdatable
? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome',
{
updateUrl: installedStyle.updateUrl
}
);
}
}
function sendEvent(type, detail = null) {
if (FIREFOX) {
type = type.replace('Chrome', '');
} else if (OPERA || VIVALDI) {
type = type.replace('Chrome', 'Opera');
}
detail = {detail};
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); // eslint-disable-line no-undef
}
onDOMready().then(() => {
document.dispatchEvent(new CustomEvent(type, detail));
});
}
function onClick(event) {
if (onClick.processing || !orphanCheck()) {
return;
}
onClick.processing = true;
(event.type.includes('Update') ? onUpdate() : onInstall())
.then(done, done);
function done() {
setTimeout(() => {
onClick.processing = false;
});
}
}
function onInstall() {
return getResource(getMeta('stylish-description'))
.then(name => saveStyleCode('styleInstall', name))
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
}
function onUpdate() {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
method: 'getStyles',
url: getMeta('stylish-id-url') || location.href,
}, ([style]) => {
saveStyleCode('styleUpdate', style.name, {id: style.id})
.then(resolve, reject);
});
});
}
function saveStyleCode(message, name, addProps) {
return new Promise((resolve, reject) => {
const isNew = message === 'styleInstall';
const needsConfirmation = isNew || !saveStyleCode.confirmed;
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
reject();
return;
}
saveStyleCode.confirmed = true;
enableUpdateButton(false);
getStyleJson().then(json => {
if (!json) {
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
'https://github.com/openstyles/stylus/issues/195');
return;
}
chrome.runtime.sendMessage(
Object.assign(json, addProps, {
method: 'saveStyle',
reason: isNew ? 'install' : 'update',
}),
style => {
if (!isNew && style.updateUrl.includes('?')) {
enableUpdateButton(true);
} else {
sendEvent('styleInstalledChrome');
}
}
);
resolve();
});
});
function enableUpdateButton(state) {
const important = s => s.replace(/;/g, '!important;');
const button = document.getElementById('update_style_button');
if (button) {
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
const icon = button.querySelector('img[src*=".svg"]');
if (icon) {
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
if (state) {
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
}
}
}
}
}
function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null;
}
function getResource(url) {
return new Promise(resolve => {
if (url.startsWith('#')) {
resolve(document.getElementById(url.slice(1)).textContent);
} else {
chrome.runtime.sendMessage({method: 'download', url}, resolve);
}
});
}
function getStyleJson() {
const url = getStyleURL();
return getResource(url).then(code => {
try {
return JSON.parse(code);
} catch (e) {
return fetch(url).then(r => r.json()).catch(() => null);
}
});
}
function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
}
if (a.length !== b.length) {
return false;
}
// order of sections should be identical to account for the case of multiple
// sections matching the same URL because the order of rules is part of cascading
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
function propertiesEqual(secA, secB) {
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
return false;
}
}
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
}
function equalOrEmpty(a, b, telltale, comparator) {
const typeA = a && typeof a[telltale] === 'function';
const typeB = b && typeof b[telltale] === 'function';
return (
(a === null || a === undefined || (typeA && !a.length)) &&
(b === null || b === undefined || (typeB && !b.length))
) || typeA && typeB && a.length === b.length && comparator(a, b);
}
function arrayMirrors(array1, array2) {
return (
array1.every(el => array2.includes(el)) &&
array2.every(el => array1.includes(el))
);
}
}
function onDOMready() {
if (document.readyState !== 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
}
function orphanCheck() {
if (chrome.i18n && chrome.i18n.getUILanguage()) {
return true;
}
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
['Update', 'Install'].forEach(type =>
['', 'Chrome', 'Opera'].forEach(browser =>
document.addEventListener('stylish' + type + browser, onClick)));
}
})();
// TODO: remove the following statement when USO is fixed
document.documentElement.appendChild(document.createElement('script')).text = '(' +
function () {
let settings;
const originalResponseJson = Response.prototype.json;
document.currentScript.remove();
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
document.removeEventListener('stylusFixBuggyUSOsettings', _);
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, ''));
if (!settings) {
Response.prototype.json = originalResponseJson;
}
});
Response.prototype.json = function (...args) {
return originalResponseJson.call(this, ...args).then(json => {
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
return json;
}
Response.prototype.json = originalResponseJson;
const images = new Map();
for (const jsonSetting of json.style_settings) {
let value = settings.get('ik-' + jsonSetting.install_key);
if (!value
|| !jsonSetting.style_setting_options
|| !jsonSetting.style_setting_options[0]) {
continue;
}
if (value.startsWith('ik-')) {
value = value.replace(/^ik-/, '');
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
if (!defaultItem || defaultItem.install_key !== value) {
if (defaultItem) {
defaultItem.default = false;
}
jsonSetting.style_setting_options.some(item => {
if (item.install_key === value) {
item.default = true;
return true;
}
});
}
} else if (jsonSetting.setting_type === 'image') {
jsonSetting.style_setting_options.some(item => {
if (item.default) {
item.default = false;
return true;
}
});
images.set(jsonSetting.install_key, value);
} else {
const item = jsonSetting.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) {
return;
}
observer.disconnect();
for (const [name, url] of images.entries()) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
});
};
} + ')()';
// TODO: remove the following statement when USO pagination is fixed
if (location.search.includes('category=')) {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
new MutationObserver((_, observer) => {
if (!document.getElementById('pagination')) {
return;
}
observer.disconnect();
const category = '&' + location.search.match(/category=[^&]+/)[0];
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
for (let i = 0; i < links.length; i++) {
links[i].href += category;
}
}).observe(document, {childList: true, subtree: true});
});
}

View File

@ -1,363 +0,0 @@
'use strict';
const CHROMIUM = chrome.app && /Chromium/.test(navigator.userAgent); // non-Windows Chromium
const FIREFOX = !chrome.app;
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
document.addEventListener('stylishUpdate', onClick);
document.addEventListener('stylishUpdateChrome', onClick);
document.addEventListener('stylishUpdateOpera', onClick);
document.addEventListener('stylishInstall', onClick);
document.addEventListener('stylishInstallChrome', onClick);
document.addEventListener('stylishInstallOpera', onClick);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// orphaned content script check
if (msg.method === 'ping') {
sendResponse(true);
}
});
// TODO: remove the following statement when USO is fixed
document.documentElement.appendChild(document.createElement('script')).text = '(' +
function () {
let settings;
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
document.removeEventListener('stylusFixBuggyUSOsettings', _);
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
});
const originalResponseJson = Response.prototype.json;
Response.prototype.json = function (...args) {
return originalResponseJson.call(this, ...args).then(json => {
Response.prototype.json = originalResponseJson;
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
return json;
}
const images = new Map();
for (const jsonSetting of json.style_settings) {
let value = settings.get('ik-' + jsonSetting.install_key);
if (!value
|| !jsonSetting.style_setting_options
|| !jsonSetting.style_setting_options[0]) {
continue;
}
if (value.startsWith('ik-')) {
value = value.replace(/^ik-/, '');
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
if (!defaultItem || defaultItem.install_key !== value) {
if (defaultItem) {
defaultItem.default = false;
}
jsonSetting.style_setting_options.some(item => {
if (item.install_key === value) {
item.default = true;
return true;
}
});
}
} else if (jsonSetting.setting_type === 'image') {
jsonSetting.style_setting_options.some(item => {
if (item.default) {
item.default = false;
return true;
}
});
images.set(jsonSetting.install_key, value);
} else {
const item = jsonSetting.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) {
return;
}
observer.disconnect();
for (const [name, url] of images.entries()) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
});
};
} + ')()';
// TODO: remove the following statement when USO pagination is fixed
if (location.search.includes('category=')) {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
new MutationObserver((_, observer) => {
if (!document.getElementById('pagination')) {
return;
}
observer.disconnect();
const category = '&' + location.search.match(/category=[^&]+/)[0];
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
for (let i = 0; i < links.length; i++) {
links[i].href += category;
}
}).observe(document, {childList: true, subtree: true});
});
}
new MutationObserver((mutations, observer) => {
if (document.body) {
observer.disconnect();
// TODO: remove the following statement when USO pagination title is fixed
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
chrome.runtime.sendMessage({
method: 'getStyles',
url: getMeta('stylish-id-url') || location.href
}, checkUpdatability);
}
}).observe(document.documentElement, {childList: true});
/* 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 url = getMeta('stylish-code-chrome');
// TODO: remove when USO is fixed
const directUrl = getMeta('stylish-update-url');
if (directUrl.includes('?') && !url.includes('?')) {
/* get custom settings from the update url */
return Object.assign(new URL(url), {
search: (new URL(directUrl)).search
}).href;
}
return url;
}
function checkUpdatability([installedStyle]) {
// TODO: remove the following statement when USO is fixed
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
detail: installedStyle && installedStyle.updateUrl,
}));
if (!installedStyle) {
sendEvent('styleCanBeInstalledChrome');
return;
}
const md5Url = getMeta('stylish-md5-url');
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
getResource(md5Url).then(md5 => {
reportUpdatable(md5 !== installedStyle.originalMd5);
});
} else {
getResource(getStyleURL()).then(code => {
reportUpdatable(code === null ||
!styleSectionsEqual(JSON.parse(code), installedStyle));
});
}
function reportUpdatable(isUpdatable) {
sendEvent(
isUpdatable
? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome',
{
updateUrl: installedStyle.updateUrl
}
);
}
}
function sendEvent(type, detail = null) {
if (FIREFOX) {
type = type.replace('Chrome', '');
} else if (OPERA || VIVALDI) {
type = type.replace('Chrome', 'Opera');
}
detail = {detail};
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); // eslint-disable-line no-undef
}
onDOMready().then(() => {
document.dispatchEvent(new CustomEvent(type, detail));
});
}
function onClick(event) {
if (onClick.processing || !orphanCheck || !orphanCheck()) {
return;
}
onClick.processing = true;
(event.type.includes('Update') ? onUpdate() : onInstall())
.then(done, done);
function done() {
setTimeout(() => {
onClick.processing = false;
});
}
}
function onInstall() {
return getResource(getMeta('stylish-description'))
.then(name => saveStyleCode('styleInstall', name))
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
}
function onUpdate() {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
method: 'getStyles',
url: getMeta('stylish-id-url') || location.href,
}, ([style]) => {
saveStyleCode('styleUpdate', style.name, {id: style.id})
.then(resolve, reject);
});
});
}
function saveStyleCode(message, name, addProps) {
return new Promise((resolve, reject) => {
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
reject();
return;
}
enableUpdateButton(false);
getResource(getStyleURL()).then(code => {
chrome.runtime.sendMessage(
Object.assign(JSON.parse(code), addProps, {
method: 'saveStyle',
reason: 'update',
}),
style => {
if (message === 'styleUpdate' && style.updateUrl.includes('?')) {
enableUpdateButton(true);
} else {
sendEvent('styleInstalledChrome');
}
}
);
resolve();
});
});
function enableUpdateButton(state) {
const button = document.getElementById('update_style_button');
if (button) {
button.style.cssText = state ? '' :
'pointer-events: none !important; opacity: .25 !important;';
}
}
}
function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null;
}
function getResource(url) {
return new Promise(resolve => {
if (url.startsWith('#')) {
resolve(document.getElementById(url.slice(1)).textContent);
} else {
chrome.runtime.sendMessage({method: 'download', url}, resolve);
}
});
}
function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
}
if (a.length !== b.length) {
return false;
}
// order of sections should be identical to account for the case of multiple
// sections matching the same URL because the order of rules is part of cascading
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
function propertiesEqual(secA, secB) {
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
return false;
}
}
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
}
function equalOrEmpty(a, b, telltale, comparator) {
const typeA = a && typeof a[telltale] === 'function';
const typeB = b && typeof b[telltale] === 'function';
return (
(a === null || a === undefined || (typeA && !a.length)) &&
(b === null || b === undefined || (typeB && !b.length))
) || typeA && typeB && a.length === b.length && comparator(a, b);
}
function arrayMirrors(array1, array2) {
return (
array1.every(el => array2.includes(el)) &&
array2.every(el => array1.includes(el))
);
}
}
function onDOMready() {
if (document.readyState !== 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
}
function orphanCheck() {
const port = chrome.runtime.connect();
if (port) {
port.disconnect();
return true;
}
// we're orphaned due to an extension update
// we can detach event listeners
document.removeEventListener('stylishUpdate', onClick);
document.removeEventListener('stylishUpdateChrome', onClick);
document.removeEventListener('stylishUpdateOpera', onClick);
document.removeEventListener('stylishInstall', onClick);
document.removeEventListener('stylishInstallChrome', onClick);
document.removeEventListener('stylishInstallOpera', onClick);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
// we can destroy global functions in this context to free up memory
[
'checkUpdatability',
'getMeta',
'getResource',
'onDOMready',
'onClick',
'onInstall',
'onUpdate',
'orphanCheck',
'saveStyleCode',
'sendEvent',
'styleSectionsEqual',
].forEach(fn => (window[fn] = null));
}

View File

@ -1,10 +0,0 @@
'use strict';
function runtimeSend(request) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
request,
({success, result}) => (success ? resolve : reject)(result)
);
});
}

View File

@ -1,6 +1,7 @@
<html id="stylus">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style id="firefox-transitions-bug-suppressor">
/* restrict to FF */
@ -26,6 +27,7 @@
<script src="edit/regexp-tester.js"></script>
<script src="edit/applies-to-line-widget.js"></script>
<script src="edit/source-editor.js"></script>
<script src="edit/colorpicker-helper.js"></script>
<script src="edit/edit.js"></script>
<script src="vendor/codemirror/lib/codemirror.js"></script>
@ -36,7 +38,7 @@
<link rel="stylesheet" href="vendor/codemirror/addon/search/matchesonscrollbar.css">
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="vendor-overwrites/codemirror/addon/search/match-highlighter.js"></script>
<script src="vendor/codemirror/addon/search/match-highlighter.js"></script>
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
<script src="vendor/codemirror/addon/search/search.js"></script>
@ -56,12 +58,11 @@
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
<script src="vendor/codemirror/addon/mode/loadmode.js"></script>
<script src="vendor/codemirror/keymap/sublime.js"></script>
<script src="vendor/codemirror/keymap/emacs.js"></script>
<script src="vendor/codemirror/keymap/vim.js"></script>
<script src="/edit/match-highlighter-helper.js"></script>
<script src="/edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css">
<link id="cm-theme" rel="stylesheet">
@ -86,8 +87,7 @@
</template>
<template data-id="section">
<div>
<label i18n-text="sectionCode"></label>
<textarea class="code"></textarea>
<label i18n-text="sectionCode" class="code-label"></label>
<br>
<div class="applies-to">
<label i18n-text="appliesLabel">
@ -143,7 +143,7 @@
<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" i18n-placeholder="styleMissingName" spellcheck="false">
<input id="name" class="style-contributor" spellcheck="false">
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div>
<div id="basic-info-enabled">
@ -156,17 +156,17 @@
</section>
<section id="actions">
<div>
<button id="save-button" title="Ctrl-S" i18n-text="styleSaveLabel"></button>
<button id="save-button" i18n-text="styleSaveLabel"></button>
<button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div>
<div>
<div id="mozilla-format-container">
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
<button id="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button>
</div>
</section>
<details id="options">
<details id="options" data-pref="editor.options.expanded">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<div class="option">
<input id="editor.lineWrapping" type="checkbox">
@ -190,6 +190,21 @@
<input id="editor.autocompleteOnTyping" type="checkbox">
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
</div>
<div class="option">
<input id="editor.colorpicker" type="checkbox">
<label for="editor.colorpicker" i18n-text="cm_colorpicker"></label>
<span class="svg-inline-wrapper" i18n-title="shortcutsNote">
<svg id="colorpicker-settings" class="svg-icon settings">
<use xlink:href="#svg-icon-settings"/>
</svg>
</span>
</div>
<div class="option usercss-only">
<input id="editor.appliesToLineWidget" type="checkbox">
<label for="editor.appliesToLineWidget"
i18n-text="appliesLineWidgetLabel"
i18n-title="appliesLineWidgetWarning"></label>
</div>
<div class="option aligned">
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
<input id="editor.tabSize" type="number" min="0">
@ -197,7 +212,9 @@
<div class="option aligned">
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
<select id="editor.keyMap"></select>
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
<span class="svg-inline-wrapper">
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</span>
</div>
<div class="option aligned">
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
@ -218,14 +235,28 @@
<option value="stylelint">Stylelint</option>
<option value="" i18n-text="genericDisabledLabel"></option>
</select>
<span class="linter-settings" i18n-title="linterConfigTooltip">
<span class="svg-inline-wrapper" i18n-title="linterConfigTooltip">
<svg id="linter-settings" class="svg-icon settings">
<use xlink:href="#svg-icon-settings"/>
</svg>&nbsp;
</svg>
</span>
</div>
</details>
<section id="lint"><h2 i18n-text="linterIssues">: <span id="issue-count"></span><svg id="lint-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2><div></div></section>
<details id="lint" class="hidden" data-pref="editor.lint.expanded">
<summary>
<h2 i18n-text="linterIssues">: <span id="issue-count"></span><!-- EAT SPACE
--><svg id="lint-help" class="svg-icon info intercepts-click">
<use xlink:href="#svg-icon-help"/>
</svg>
</h2>
</summary>
<div></div>
</details>
<div id="footer">
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n-text="externalUsercssDocument"
target="_blank"></a>
</div>
</div>
<section id="sections">
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
@ -236,17 +267,17 @@
</div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
<symbol id="svg-icon-external-link" viewBox="0 0 8 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" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt">
<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" height="16" width="12" viewBox="0 0 12 16">
<symbol id="svg-icon-close" viewBox="0 0 12 16">
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
</symbol>
<symbol id="svg-icon-settings" height="12" width="12" viewBox="0 0 16 16">
<path d="M16 9.45V6.52l-1.8-.3c-.14-.5-.34-.95-.56-1.35L14.7 3.4l-2.07-2.1-1.5 1.03c-.43-.24-.9-.43-1.37-.56L9.46 0H6.54l-.3 1.8c-.5.13-.94.33-1.36.55L3.4 1.32 1.3 3.4l1.07 1.45c-.24.45-.44.92-.57 1.42L0 6.53v2.95l1.8.3c.14.52.33.95.57 1.37l-1.07 1.5 2.06 2.08 1.5-1.07c.44.24.9.45 1.4.57l.3 1.77H9.5l.32-1.8c.47-.13.95-.32 1.34-.56l1.5 1.06 2.07-2.05-1.03-1.5c.24-.45.44-.9.56-1.36L16 9.44v-.02zm-8 1.6C6.3 11.05 4.93 9.7 4.93 8S6.33 4.9 8 4.9s3.06 1.4 3.06 3.1S9.7 11.04 8 11.04z"/>
<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>
</svg>

View File

@ -1,6 +1,16 @@
/* global regExpTester debounce messageBox */
/* global regExpTester debounce messageBox CodeMirror */
'use strict';
function templateCache(cache) {
function clone(id) {
if (typeof cache[id] === 'function') {
cache[id] = cache[id]();
}
return cache[id].cloneNode(true);
}
return {clone};
}
function createAppliesToLineWidget(cm) {
const APPLIES_TYPE = [
[t('appliesUrlOption'), 'url'],
@ -13,6 +23,57 @@ function createAppliesToLineWidget(cm) {
let fromLine, toLine, styleVariables;
let initialized = false;
const template = templateCache({
container: () =>
$element({className: 'applies-to', appendChild: [
$element({tag: 'label', appendChild: t('appliesLabel')}),
$element({
tag: 'ul',
className: 'applies-to-list'
})
]}),
listItem: () =>
$element({tag: 'li', appendChild: [
$element({
tag: 'select',
className: 'applies-type',
appendChild: APPLIES_TYPE.map(([label, value]) => $element({
tag: 'option',
value: value,
textContent: label
}))
}),
$element({
tag: 'input',
className: 'applies-value'
}),
$element({
tag: 'button',
type: 'button',
className: 'applies-to-regexp-test',
textContent: t('styleRegexpTestButton')
}),
$element({
tag: 'button',
type: 'button',
className: 'applies-to-remove',
textContent: t('appliesRemove')
}),
$element({
tag: 'button',
type: 'button',
className: 'applies-to-add',
textContent: t('appliesAdd')
})
]}),
appliesToEverything: () =>
$element({
tag: 'li',
className: 'applies-to-everything',
textContent: t('appliesToEverything')
})
});
return {toggle};
function toggle(newState = !initialized) {
@ -56,13 +117,19 @@ function createAppliesToLineWidget(cm) {
styleVariables.remove();
}
function onChange(cm, {from, to, origin}) {
function onChange(cm, event) {
const {from, to, origin} = event;
if (origin === 'appliesTo') {
return;
}
const lastChanged = CodeMirror.changeEnd(event).line;
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
toLine = Math.max(toLine === null ? to.line : toLine, to.line);
debounce(update, THROTTLE_DELAY);
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
if (origin === 'setValue') {
update();
} else {
debounce(update, THROTTLE_DELAY);
}
}
function onOptionChange(cm, option) {
@ -82,9 +149,9 @@ function createAppliesToLineWidget(cm) {
function update() {
const changed = {fromLine, toLine};
fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo);
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
const visible = {fromLine, toLine};
if (fromLine >= cm.display.viewFrom && toLine <= cm.display.viewTo) {
if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
cm.operation(doUpdate);
}
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
@ -150,24 +217,40 @@ function createAppliesToLineWidget(cm) {
}
// decide search range
const fromIndex = widgets[i] ? cm.indexFromPos({line: widgets[i].line.lineNo(), ch: 0}) : 0;
const toIndex = cm.indexFromPos({line: widgets[j] ? widgets[j].line.lineNo() : toLine, ch: 0});
const fromPos = {line: widgets[i] ? widgets[i].line.lineNo() : 0, ch: 0};
const toPos = {line: widgets[j] ? widgets[j].line.lineNo() : toLine + 1, ch: 0};
// calc index->pos lookup table
let line = 0;
let index = 0;
let fromIndex, toIndex;
const lineIndexes = [index];
cm.doc.iter(({text}) => {
fromIndex = line === fromPos.line ? index : fromIndex;
lineIndexes.push((index += text.length + 1));
line++;
toIndex = line >= toPos.line ? index : toIndex;
return toIndex;
});
// splice
i = Math.max(0, i);
widgets.splice(i, 0, ...createWidgets(fromIndex, toIndex, widgets.splice(i, j - i)));
widgets.splice(i, 0, ...createWidgets(fromIndex, toIndex, widgets.splice(i, j - i), lineIndexes));
fromLine = null;
toLine = null;
}
function *createWidgets(start, end, removed) {
function *createWidgets(start, end, removed, lineIndexes) {
let i = 0;
let itemHeight;
for (const section of findAppliesTo(start, end)) {
while (removed[i] && removed[i].line.lineNo() < section.pos.line) {
clearWidget(removed[i++]);
}
setupMarkers(section);
for (const a of section.applies) {
setupApplyMarkers(a, lineIndexes);
}
if (removed[i] && removed[i].line.lineNo() === section.pos.line) {
// reuse old widget
removed[i].section.applies.forEach(apply => {
@ -176,7 +259,9 @@ function createAppliesToLineWidget(cm) {
});
removed[i].section = section;
const newNode = buildElement(section);
removed[i].node.parentNode.replaceChild(newNode, removed[i].node);
if (removed[i].node.parentNode) {
removed[i].node.parentNode.replaceChild(newNode, removed[i].node);
}
removed[i].node = newNode;
removed[i].changed();
yield removed[i];
@ -187,9 +272,11 @@ function createAppliesToLineWidget(cm) {
const widget = cm.addLineWidget(section.pos.line, buildElement(section), {
coverGutter: true,
noHScroll: true,
above: true
above: true,
height: itemHeight ? section.applies.length * itemHeight : undefined,
});
widget.section = section;
itemHeight = itemHeight || widget.node.offsetHeight / (section.applies.length || 1);
yield widget;
}
removed.slice(i).forEach(clearWidget);
@ -206,151 +293,137 @@ function createAppliesToLineWidget(cm) {
apply.mark.clear();
}
function setupMarkers({applies}) {
applies.forEach(setupApplyMarkers);
}
function setupApplyMarkers(apply) {
function setupApplyMarkers(apply, lineIndexes) {
apply.type.mark = cm.markText(
cm.posFromIndex(apply.type.start),
cm.posFromIndex(apply.type.end),
posFromIndex(cm, apply.type.start, lineIndexes),
posFromIndex(cm, apply.type.end, lineIndexes),
{clearWhenEmpty: false}
);
apply.value.mark = cm.markText(
cm.posFromIndex(apply.value.start),
cm.posFromIndex(apply.value.end),
posFromIndex(cm, apply.value.start, lineIndexes),
posFromIndex(cm, apply.value.end, lineIndexes),
{clearWhenEmpty: false}
);
apply.mark = cm.markText(
cm.posFromIndex(apply.start),
cm.posFromIndex(apply.end),
posFromIndex(cm, apply.start, lineIndexes),
posFromIndex(cm, apply.end, lineIndexes),
{clearWhenEmpty: false}
);
}
function posFromIndex(cm, index, lineIndexes) {
if (!lineIndexes) {
return cm.posFromIndex(index);
}
let line = lineIndexes.prev || 0;
const prev = lineIndexes[line];
const next = lineIndexes[line + 1];
if (prev <= index && index < next) {
return {line, ch: index - prev};
}
let a = index < prev ? 0 : line;
let b = index < next ? line + 1 : lineIndexes.length - 1;
while (a < b - 1) {
const mid = (a + b) >> 1;
if (lineIndexes[mid] < index) {
a = mid;
} else {
b = mid;
}
}
line = lineIndexes[b] > index ? a : b;
Object.defineProperty(lineIndexes, 'prev', {value: line, configurable: true});
return {line, ch: index - lineIndexes[line]};
}
function buildElement({applies}) {
const el = $element({className: 'applies-to', appendChild: [
$element({tag: 'label', appendChild: [
t('appliesLabel'),
// $element({tag: 'svg'})
]}),
$element({
tag: 'ul',
className: 'applies-to-list',
appendChild: applies.map(makeLi)
})
]});
if (!$('li', el)) {
$('ul', el).appendChild($element({
tag: 'li',
className: 'applies-to-everything',
textContent: t('appliesToEverything')
}));
const el = template.clone('container');
const appliesToList = $('.applies-to-list', el);
applies.map(makeLi)
.forEach(item => appliesToList.appendChild(item));
if (!appliesToList.childNodes.length) {
appliesToList.appendChild(template.clone('appliesToEverything'));
}
return el;
function makeLi(apply) {
const el = $element({tag: 'li', appendChild: makeInput(apply)});
const el = template.clone('listItem');
el.dataset.type = apply.type.text;
el.addEventListener('change', e => {
if (e.target.classList.contains('applies-type')) {
el.dataset.type = apply.type.text;
}
});
return el;
}
function makeInput(apply) {
const typeInput = $element({
tag: 'select',
className: 'applies-type',
appendChild: APPLIES_TYPE.map(([label, value]) => $element({
tag: 'option',
value: value,
textContent: label
})),
onchange() {
applyChange(apply.type, this.value);
}
});
const typeInput = $('.applies-type', el);
typeInput.value = apply.type.text;
const valueInput = $element({
tag: 'input',
className: 'applies-value',
value: apply.value.text,
oninput() {
debounce(applyChange, THROTTLE_DELAY, apply.value, this.value);
},
onfocus: updateRegexpTest
});
const regexpTestButton = $element({
tag: 'button',
type: 'button',
className: 'applies-to-regexp-test',
textContent: t('styleRegexpTestButton'),
onclick() {
regExpTester.toggle();
regExpTester.update([apply.value.text]);
typeInput.onchange = function () {
applyChange(apply.type, this.value);
};
const valueInput = $('.applies-value', el);
valueInput.value = apply.value.text;
valueInput.oninput = function () {
debounce(applyChange, THROTTLE_DELAY, apply.value, this.value);
};
valueInput.onfocus = updateRegexpTest;
const regexpTestButton = $('.applies-to-regexp-test', el);
regexpTestButton.onclick = () => {
regExpTester.toggle();
regExpTester.update([apply.value.text]);
};
const removeButton = $('.applies-to-remove', el);
removeButton.onclick = function () {
const i = applies.indexOf(apply);
let repl;
let from;
let to;
if (applies.length < 2) {
messageBox({
contents: chrome.i18n.getMessage('appliesRemoveError'),
buttons: [t('confirmClose')]
});
return;
}
});
const removeButton = $element({
tag: 'button',
type: 'button',
className: 'applies-to-remove',
textContent: t('appliesRemove'),
onclick() {
const i = applies.indexOf(apply);
let repl;
let from;
let to;
if (applies.length < 2) {
messageBox({
contents: chrome.i18n.getMessage('appliesRemoveError'),
buttons: [t('confirmClose')]
});
return;
}
if (i === 0) {
from = apply.mark.find().from;
to = applies[i + 1].mark.find().from;
repl = '';
} else if (i === applies.length - 1) {
from = applies[i - 1].mark.find().to;
to = apply.mark.find().to;
repl = '';
} else {
from = applies[i - 1].mark.find().to;
to = applies[i + 1].mark.find().from;
repl = ', ';
}
cm.replaceRange(repl, from, to, 'appliesTo');
clearApply(apply);
this.closest('li').remove();
applies.splice(i, 1);
if (i === 0) {
from = apply.mark.find().from;
to = applies[i + 1].mark.find().from;
repl = '';
} else if (i === applies.length - 1) {
from = applies[i - 1].mark.find().to;
to = apply.mark.find().to;
repl = '';
} else {
from = applies[i - 1].mark.find().to;
to = applies[i + 1].mark.find().from;
repl = ', ';
}
});
const addButton = $element({
tag: 'button',
type: 'button',
className: 'applies-to-add',
textContent: t('appliesAdd'),
onclick() {
const i = applies.indexOf(apply);
const pos = apply.mark.find().to;
const text = `, ${apply.type.text}("")`;
cm.replaceRange(text, pos, pos, 'appliesTo');
const newApply = createApply(
cm.indexFromPos(pos) + 2,
apply.type.text,
'',
true
);
setupApplyMarkers(newApply);
applies.splice(i + 1, 0, newApply);
this.closest('li').insertAdjacentElement('afterend', makeLi(newApply));
}
});
return [typeInput, valueInput, regexpTestButton, removeButton, addButton];
cm.replaceRange(repl, from, to, 'appliesTo');
clearApply(apply);
this.closest('li').remove();
applies.splice(i, 1);
};
const addButton = $('.applies-to-add', el);
addButton.onclick = function () {
const i = applies.indexOf(apply);
const pos = apply.mark.find().to;
const text = `, ${apply.type.text}("")`;
cm.replaceRange(text, pos, pos, 'appliesTo');
const newApply = createApply(
cm.indexFromPos(pos) + 2,
apply.type.text,
'',
true
);
setupApplyMarkers(newApply);
applies.splice(i + 1, 0, newApply);
this.closest('li').insertAdjacentElement('afterend', makeLi(newApply));
};
return el;
function updateRegexpTest() {
if (apply.type.text === 'regexp') {

View File

@ -1,3 +1,6 @@
.CodeMirror-hints {
z-index: 999;
}
.CodeMirror-hint:hover {
color: white;
background: #08f;
@ -31,3 +34,14 @@
.CodeMirror-search-hint {
color: #888;
}
.cm-uso-variable {
font-weight: bold;
}
.cm-searching.cm-matchhighlight {
/* tokens found by manual search should not animate by cm-matchhighlight */
animation-name: search-and-match-highlighter !important;
}
@keyframes search-and-match-highlighter {
from { background-color: rgba(255, 255, 0, .4); } /* search color */
to { background-color: rgba(100, 255, 100, .4); } /* sarch + highlight */
}

View File

@ -1,4 +1,4 @@
/* global CodeMirror prefs */
/* global CodeMirror prefs loadScript editor */
'use strict';
@ -100,16 +100,31 @@
});
}
CodeMirror.modeURL = '/vendor/codemirror/mode/%N/%N.js';
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
'mix-blend-mode': true,
'isolation': true,
});
Object.assign(CodeMirror.mimeModes['text/css'].valueKeywords, {
'isolate': true,
});
const MODE = {
stylus: 'stylus',
uso: 'css'
};
CodeMirror.defineExtension('setPreprocessor', function (preprocessor) {
this.setOption('mode', MODE[preprocessor] || 'css');
CodeMirror.autoLoadMode(this, MODE[preprocessor] || 'css');
CodeMirror.defineExtension('setPreprocessor', function (preprocessor, force = false) {
const mode = MODE[preprocessor] || 'css';
if ((this.doc.mode || {}).name === mode && !force) {
return Promise.resolve();
}
if (mode === 'css') {
this.setOption('mode', mode);
return Promise.resolve();
}
return loadScript(`/vendor/codemirror/mode/${mode}/${mode}.js`).then(() => {
this.setOption('mode', mode);
});
});
CodeMirror.defineExtension('isBlank', function () {
@ -124,3 +139,65 @@
return isBlank;
});
})();
// eslint-disable-next-line no-unused-expressions
CodeMirror.hint && (() => {
const USO_VAR = 'uso-variable';
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
const USO_INVALID_VAR = 'error ' + USO_VAR;
const originalHelper = CodeMirror.hint.css || (() => {});
CodeMirror.registerHelper('hint', 'css', function (cm) {
const {line, ch} = cm.getCursor();
const {styles, text} = cm.getLineHandle(line);
if (!styles || !editor) {
return originalHelper(cm);
}
let prev = 0;
for (let i = 1; i < styles.length; i += 2) {
let end = styles[i];
if (prev <= ch && ch <= end &&
(styles[i + 1] || '').includes(USO_VAR)) {
const adjust = text[prev] === '/' ? 4 : 0;
prev += adjust;
end -= adjust;
const leftPart = text.slice(prev, ch);
const list = Object.keys(editor.getStyle().usercssData.vars)
.filter(name => name.startsWith(leftPart));
return {
list,
from: {line, ch: prev},
to: {line, ch: end},
};
}
prev = end;
}
return originalHelper(cm);
});
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks;
const originalCommentHook = hooks['/'];
hooks['/'] = tokenizeUsoVariables;
function tokenizeUsoVariables(stream) {
const token = originalCommentHook.apply(this, arguments);
if (token[1] !== 'comment') {
return token;
}
const {string, start, pos} = stream;
// /*[[install-key]]*/
// 01234 43210
if (string[start + 2] === '[' &&
string[start + 3] === '[' &&
string[pos - 3] === ']' &&
string[pos - 4] === ']') {
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
if (vars && Object.hasOwnProperty.call(vars, string.slice(start + 4, pos - 4))) {
token[0] = USO_VALID_VAR;
} else {
token[0] = USO_INVALID_VAR;
}
}
return token;
}
})();

153
edit/colorpicker-helper.js Normal file
View File

@ -0,0 +1,153 @@
/* global CodeMirror loadScript editors showHelp */
'use strict';
// eslint-disable-next-line no-var
var initColorpicker = () => {
initOverlayHooks();
onDOMready().then(() => {
$('#colorpicker-settings').onclick = configureColorpicker;
});
const scripts = [
'/vendor-overwrites/colorpicker/colorpicker.css',
'/vendor-overwrites/colorpicker/colorpicker.js',
'/vendor-overwrites/colorpicker/colorview.js',
];
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
prefs.subscribe(['editor.colorpicker'], colorpickerOnDemand);
return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true);
function colorpickerOnDemand(id, enabled) {
return loadScript(enabled && scripts)
.then(() => setColorpickerOption(id, enabled));
}
function setColorpickerOption(id, enabled) {
const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey');
delete defaults.extraKeys[keyName];
defaults.colorpicker = enabled;
if (enabled) {
if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys[keyName] = 'colorpicker';
}
defaults.colorpicker = {
forceUpdate: editors.length > 0,
tooltip: t('colorpickerTooltip'),
popupOptions: {
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
hideDelay: 5000,
embedderCallback: state => {
['hexUppercase', 'color']
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
},
},
};
}
// on page load runs before CodeMirror.setOption is defined
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
}
function registerHotkey(id, hotkey) {
CodeMirror.commands.colorpicker = invokeColorpicker;
const extraKeys = CodeMirror.defaults.extraKeys;
for (const key in extraKeys) {
if (extraKeys[key] === 'colorpicker') {
delete extraKeys[key];
break;
}
}
if (hotkey) {
extraKeys[hotkey] = 'colorpicker';
}
}
function invokeColorpicker(cm) {
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
}
function configureColorpicker() {
const input = $element({
tag: 'input',
type: 'search',
spellcheck: false,
value: prefs.get('editor.colorpicker.hotkey'),
onkeydown(event) {
const key = CodeMirror.keyName(event);
// ignore: [Shift?] characters, modifiers-only, [Shift?] Esc, Enter, [Shift?] Tab
if (key === 'Enter' || key === 'Esc') {
$('#help-popup .dismiss').onclick();
return;
} else if (/^(Space|(Shift-)?(Esc|Tab|[!-~])|(Shift-?|Ctrl-?|Alt-?|Cmd-?)*)$/.test(key)) {
this.setCustomValidity('Not allowed');
} else {
this.setCustomValidity('');
prefs.set('editor.colorpicker.hotkey', key);
}
event.preventDefault();
event.stopPropagation();
this.value = key;
},
oninput() {
// fired on pressing "x" to clear the field
prefs.set('editor.colorpicker.hotkey', '');
},
onpaste(event) {
event.preventDefault();
}
});
const popup = showHelp(t('helpKeyMapHotkey'), input);
if (this instanceof Element) {
const bounds = this.getBoundingClientRect();
popup.style.left = bounds.right + 10 + 'px';
popup.style.top = bounds.top - popup.clientHeight / 2 + 'px';
popup.style.right = 'auto';
}
input.focus();
}
function initOverlayHooks() {
const COLORVIEW_DISABLED_SUFFIX = ' colorview-disabled';
const COLORVIEW_NEXT_DISABLED_SUFFIX = ' colorview-next-disabled';
const originalAddOverlay = CodeMirror.prototype.addOverlay;
CodeMirror.prototype.addOverlay = addOverlayHook;
function addOverlayHook(overlay) {
if (overlay.token !== tokenHook && (
overlay === (this.state.matchHighlighter || {}).overlay ||
overlay === (this.state.search || {}).overlay)) {
overlay.colopickerHelper = {token: overlay.token};
overlay.token = tokenHook;
}
originalAddOverlay.apply(this, arguments);
}
function tokenHook(stream) {
const style = this.colopickerHelper.token.apply(this, arguments);
if (!style) {
return style;
}
const {start, pos, lineOracle: {baseTokens}} = stream;
if (!baseTokens) {
return style;
}
for (let prev = 0, i = 1; i < baseTokens.length; i += 2) {
const end = baseTokens[i];
if (prev <= start && start <= end) {
const base = baseTokens[i + 1];
if (base && base.includes('colorview')) {
return style +
(start > prev ? COLORVIEW_DISABLED_SUFFIX : '') +
(pos < end ? COLORVIEW_NEXT_DISABLED_SUFFIX : '');
}
} else if (end > pos) {
break;
}
prev = end;
}
return style;
}
}
};

View File

@ -1,3 +1,7 @@
:root {
--header-narrow-min-height: 12em;
}
body {
margin: 0;
font: 12px arial,sans-serif;
@ -19,6 +23,10 @@ body {
opacity: 1;
}
.hidden {
display: none !important;
}
/************ header ************/
#header {
width: 280px;
@ -29,7 +37,11 @@ body {
padding: 15px;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 3rem -1.2rem black;
box-shadow: 0 0 3rem -1.2rem black;
box-sizing: border-box;
z-index: 10;
display: flex;
flex-direction: column;
}
#header h1 {
margin-top: 0;
@ -44,7 +56,7 @@ body {
.aligned {
display: table-row;
}
.aligned > *:not(svg) {
.aligned > *:not(.svg-inline-wrapper) {
display: table-cell;
margin-top: 0.1rem;
min-height: 1.4rem;
@ -83,15 +95,12 @@ input[type="checkbox"] {
transition: fill .5s;
width: 16px;
height: 16px;
}
.svg-icon:not(.dismiss) {
margin-left: 0.2rem;
}
h2 .svg-icon, label .svg-icon {
margin-top: -1px;
}
.svg-icon.info,
.svg-icon.settings {
.svg-icon.info {
width: 14px;
height: 16px;
}
@ -105,6 +114,10 @@ h2 .svg-icon, label .svg-icon {
.svg-icon.settings:hover {
fill: #000;
}
input:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
#enabled {
margin-left: 0;
vertical-align: middle;
@ -117,22 +130,36 @@ h2 .svg-icon, label .svg-icon {
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
/* options */
#options summary {
/* collapsibles */
#header summary {
align-items: center;
margin-left: -13px;
cursor: pointer;
outline: none;
}
#options summary h2 {
#header summary h2 {
display: inline-block;
border-bottom: 1px dotted transparent;
}
#header summary h2:hover {
border-color: #bbb;
}
#header summary svg {
margin-top: -3px;
}
#options:not([open]) + #lint h2 {
margin-top: 0;
}
#lint:not([open]) h2 {
margin-bottom: 0;
}
/* options */
#options [type="number"] {
width: 3.5em;
text-align: left;
padding-left: .25em;
}
#options .option > * {
#options .option.aligned > * {
padding-right: 0.25rem;
}
.set-option-progress {
@ -144,6 +171,10 @@ h2 .svg-icon, label .svg-icon {
/* footer */
#footer {
margin-top: 1em;
margin-bottom: .5em;
}
#lint:not([open]) + #footer {
margin-top: 4em;
}
/************ content ***********/
#sections > div {
@ -368,7 +399,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
#help-popup.big {
box-shadow: rgba(0, 0, 0, 0.45) 0px 0px 0px 100000px !important;
max-width: 100%;
left: 3rem;
left: calc(280px - 3rem);
}
#help-popup.big .CodeMirror {
min-height: 2rem;
@ -400,8 +431,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
margin-left: 10px;
font-weight: bold;
}
#help-popup .saved-message.show,
#options .linter-settings {
#help-popup .saved-message.show {
display: inline-block;
}
@ -444,10 +474,14 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
/************ lint ************/
#lint {
display: none;
overflow-y: auto;
overflow-x: hidden;}
#lint > summary {
/* workaround for overflow:auto to show the toggle triangle */
position: absolute;
}
#lint > div {
overflow-y: auto;
margin-top: 4em;
}
#lint table {
font-size: 100%;
@ -483,6 +517,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
}
#lint td[role="message"] {
text-align: left;
white-space: nowrap;
}
#message-box.center.lint-config #message-box-contents {
text-align: left;
@ -517,13 +552,46 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
}
/************ single editor **************/
.usercss body {
display: flex;
height: 100vh;
flex-direction: column;
justify-items: normal;
}
html:not(.usercss) .usercss-only,
.usercss #mozilla-format-container,
.usercss #sections > h2 {
display: none !important; /* hide during page init */
}
#sections .single-editor {
margin: 0;
height: 100%;
padding: 0;
display: flex;
box-sizing: border-box;
}
.single-editor .CodeMirror {
width: 100%;
height: auto;
border: none;
outline: none;
}
#footer a {
color: #333;
transition: color .5s;
text-decoration-skip: ink;
}
#footer a:hover {
color: #666;
}
.usercss.firefox #sections,
.usercss.firefox .single-editor,
.usercss.firefox .CodeMirror {
height: 100%;
}
@ -558,6 +626,8 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
position: inherit;
border-right: none;
border-bottom: 1px dashed #AAA;
min-height: var(--header-narrow-min-height);
max-height: 50vh;
}
#header section:not(:last-child) {
margin-bottom: 0.4rem;
@ -601,7 +671,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
#options h2 {
margin: 0 0 .5em;
}
#options .aligned > *:not(svg) {
#options .aligned > *:not(.svg-inline-wrapper) {
margin: 1px 0 0 0; /* workaround the flowing-padding column bug in webkit */
padding-right: 0.4rem;
vertical-align: baseline;
@ -619,22 +689,12 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
position: relative;
top: 0.2rem;
}
#lint h2 {
display: block;
cursor: default;
margin-top: 0;
margin-bottom: 0;
#options:not([open]) ~ #lint {
margin-top: -1ex;
}
#lint > div {
max-height: 20vh;
}
#lint.collapsed > div {
display: none;
}
#lint:hover > div {
margin-top: 1em;
max-height: 30vh;
}
#lint table {
width: 100%;
}
@ -653,6 +713,16 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
.applies-type {
width: 30%;
}
.usercss .CodeMirror-scroll {
max-height: calc(100vh - var(--header-narrow-min-height));
}
.usercss #options:not([open]) ~ #lint.hidden ~ #footer,
.usercss #lint:not([open]) + #footer {
margin-top: -.25em;
}
#help-popup.big {
left: 3rem;
}
}
@media(max-width:500px) {
#options {

View File

@ -5,6 +5,8 @@
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
/* global mozParser createSourceEditor */
/* global closeCurrentTab regExpTester messageBox */
/* global initColorpicker */
/* global initCollapsibles */
'use strict';
let styleId = null;
@ -22,25 +24,59 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do
let editor;
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
onBackgroundReady();
Promise.all([
initStyleData().then(style => {
styleId = style.id;
sessionStorage.justEditedStyleId = styleId;
// we set "usercss" class on <html> when <body> is empty
// so there'll be no flickering of the elements that depend on it
if (isUsercss(style)) {
document.documentElement.classList.add('usercss');
}
// strip URL parameters when invoked for a non-existent id
if (!styleId) {
history.replaceState({}, document.title, location.pathname);
}
return style;
}),
onDOMready(),
onBackgroundReady(),
])
.then(([style]) => Promise.all([
style,
initColorpicker(),
initCollapsibles(),
initHooksCommon(),
]))
.then(([style]) => {
initCodeMirror();
const usercss = isUsercss(style);
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
if (usercss) {
editor = createSourceEditor(style);
} else {
initWithSectionStyle({style});
}
// workaround part2 for the <details> not showing its toggle icon: hide <summary> on scroll
$('#lint').addEventListener('scroll', function () {
const newOpacity = this.scrollTop === 0 ? '' : '0';
const style = this.firstElementChild.style;
if (style.opacity !== newOpacity) {
style.opacity = newOpacity;
}
}, {passive: true});
});
// make querySelectorAll enumeration code readable
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
NodeList.prototype[method] = Array.prototype[method];
});
// Chrome pre-34
Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector;
// Chrome pre-41 polyfill
Element.prototype.closest = Element.prototype.closest || function (selector) {
let e;
// eslint-disable-next-line no-empty
for (e = this; e && !e.matches(selector); e = e.parentElement) {}
return e;
};
// eslint-disable-next-line no-extend-native
Array.prototype.rotate = function (amount) {
// negative amount == rotate left
@ -70,6 +106,7 @@ const hotkeyRerouter = {
save: true, jumpToLine: true, nextEditor: true, prevEditor: true,
find: true, findNext: true, findPrev: true, replace: true, replaceAll: true,
toggleStyle: true,
colorpicker: true,
},
setState: enable => {
setTimeout(() => {
@ -309,7 +346,8 @@ function acmeEventListener(event) {
let value = el.type === 'checkbox' ? el.checked : el.value;
switch (option) {
case 'tabSize':
CodeMirror.setOption('indentUnit', Number(value));
value = Number(value);
CodeMirror.setOption('indentUnit', value);
break;
case 'theme': {
const themeLink = $('#cm-theme');
@ -362,13 +400,18 @@ function acmeEventListener(event) {
}
option = 'highlightSelectionMatches';
break;
case 'colorpicker':
return;
}
CodeMirror.setOption(option, value);
}
// replace given textarea with the CodeMirror editor
function setupCodeMirror(textarea, index) {
const cm = CodeMirror.fromTextArea(textarea, {lint: null});
function setupCodeMirror(sectionDiv, code, index) {
const cm = CodeMirror(wrapper => {
$('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper);
}, {
value: code,
});
const wrapper = cm.display.wrapper;
cm.on('changes', indicateCodeChangeDebounced);
@ -490,7 +533,7 @@ document.addEventListener('wheel', event => {
}
});
queryTabs({currentWindow: true}).then(tabs => {
chrome.windows && queryTabs({currentWindow: true}).then(tabs => {
const windowId = tabs[0].windowId;
if (prefs.get('openEditInWindow')) {
if (
@ -517,6 +560,9 @@ queryTabs({currentWindow: true}).then(tabs => {
getOwnTab().then(tab => {
const ownTabId = tab.id;
useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href;
if (!chrome.windows) {
return;
}
// When an edit page gets attached or detached, remember its state
// so we can do the same to the next one to open.
chrome.tabs.onAttached.addListener((tabId, info) => {
@ -631,12 +677,12 @@ function addSection(event, section) {
$('.add-section', div).addEventListener('click', addSection, false);
$('.beautify-section', div).addEventListener('click', beautify);
const codeElement = $('.code', div);
const code = (section || {}).code || '';
const appliesTo = $('.applies-to-list', div);
let appliesToAdded = false;
if (section) {
codeElement.value = section.code;
for (const i in propertyToCss) {
if (section[i]) {
section[i].forEach(url => {
@ -689,13 +735,13 @@ function addSection(event, section) {
const clickedSection = getSectionForChild(event.target);
sections.insertBefore(div, clickedSection.nextElementSibling);
const newIndex = getSections().indexOf(clickedSection) + 1;
cm = setupCodeMirror(codeElement, newIndex);
cm = setupCodeMirror(div, code, newIndex);
makeSectionVisible(cm);
cm.focus();
renderLintReport();
cm.focus();
} else {
sections.appendChild(div);
cm = setupCodeMirror(codeElement);
cm = setupCodeMirror(div, code);
}
div.CodeMirror = cm;
setCleanSection(div);
@ -1058,7 +1104,7 @@ function autocompleteOnTyping(cm, [info], debounced) {
debounce(autocompleteOnTyping, 100, cm, [info], true);
return;
}
if (info.text.last.match(/[-\w!]+$/)) {
if (info.text.last.match(/[-a-z!]+$/i)) {
cm.state.autocompletePicked = false;
cm.options.hintOptions.completeSingle = false;
cm.execCommand('autocomplete');
@ -1298,56 +1344,26 @@ function beautify(event) {
}
}
onDOMready().then(init);
function init() {
initCodeMirror();
getStyle().then(style => {
styleId = style.id;
sessionStorage.justEditedStyleId = styleId;
if (!isUsercss(style)) {
initWithSectionStyle({style});
} else {
editor = createSourceEditor(style);
}
function initStyleData() {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const id = params.get('id');
const createEmptyStyle = () => ({
id: null,
name: '',
enabled: true,
sections: [
Object.assign({code: ''},
...Object.keys(CssToProperty)
.map(name => ({
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
}))
)
],
});
function getStyle() {
const id = new URLSearchParams(location.search).get('id');
if (!id) {
// match should be 2 - one for the whole thing, one for the parentheses
// This is an add
$('#heading').textContent = t('addStyleTitle');
return Promise.resolve(createEmptyStyle());
}
$('#heading').textContent = t('editStyleHeading');
// This is an edit
return getStylesSafe({id}).then(styles => {
let style = styles[0];
if (!style) {
style = createEmptyStyle();
history.replaceState({}, document.title, location.pathname);
}
return style;
});
}
function createEmptyStyle() {
const params = new URLSearchParams(location.search);
const style = {
id: null,
name: '',
enabled: true,
sections: [{code: ''}]
};
for (const i in CssToProperty) {
if (params.get(i)) {
style.sections[0][CssToProperty[i]] = [params.get(i)];
}
}
return style;
}
return !id ?
Promise.resolve(createEmptyStyle()) :
getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
}
function setStyleMeta(style) {
@ -1423,16 +1439,6 @@ function addSections(sections, onAdded = () => {}) {
}
}
function setupOptionsExpand() {
$('#options').open = prefs.get('editor.options.expanded');
$('#options h2').addEventListener('click', () => {
setTimeout(() => prefs.set('editor.options.expanded', $('#options').open));
});
prefs.subscribe(['editor.options.expanded'], (key, value) => {
$('#options').open = value;
});
}
function initHooks() {
if (initHooks.alreadyDone) {
return;
@ -1452,7 +1458,6 @@ function initHooks() {
$('#keyMap-help').addEventListener('click', showKeyMapHelp, false);
$('#cancel-button').addEventListener('click', goBackToManage);
setupOptionsExpand();
initLint();
if (!FIREFOX) {
@ -1465,17 +1470,36 @@ function initHooks() {
).forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
}
window.addEventListener('load', function _() {
window.removeEventListener('load', _);
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
});
setupGlobalSearch();
}
// common for usercss and classic
function initHooksCommon() {
showKeyInSaveButtonTooltip();
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
function showKeyInSaveButtonTooltip(prefName, value) {
$('#save-button').title = findKeyForCommand('save', value);
}
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
const map = CodeMirror.keyMap[mapName];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
}
function toggleContextMenuDelete(event) {
if (event.button === 2 && prefs.get('editor.contextDelete')) {
if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
chrome.contextMenus.update('editor.contextDelete', {
enabled: Boolean(
this.selectionStart !== this.selectionEnd ||
@ -1589,11 +1613,6 @@ function save() {
function saveSectionStyle() {
updateLintReportIfEnabled(null, 0);
// save the contents of the CodeMirror editors back into the textareas
for (let i = 0; i < editors.length; i++) {
editors[i].save();
}
const error = validate();
if (error) {
alert(error);
@ -1671,14 +1690,14 @@ function fromMozillaFormat() {
tag: 'button',
name: 'import-append',
textContent: t('importAppendLabel'),
title: t('importAppendTooltip'),
title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
onclick: doImport,
}),
$element({
tag: 'button',
name: 'import-replace',
textContent: t('importReplaceLabel'),
title: t('importReplaceTooltip'),
title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
onclick: () => doImport({replaceOldStyle: true}),
}),
]}));
@ -1687,6 +1706,7 @@ function fromMozillaFormat() {
popup.codebox.focus();
popup.codebox.on('changes', cm => {
popup.classList.toggle('ready', !cm.isBlank());
cm.markClean();
});
// overwrite default extraKeys as those are inapplicable in popup context
popup.codebox.options.extraKeys = {
@ -1854,33 +1874,39 @@ function showKeyMapHelp() {
}
}
function showHelp(title, body) {
function showHelp(title = '', body) {
const div = $('#help-popup');
div.classList.remove('big');
$('.contents', div).textContent = '';
$('.contents', div).appendChild(typeof body === 'string' ? tHTML(body) : body);
const contents = $('.contents', div);
contents.textContent = '';
if (body) {
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
}
$('.title', div).textContent = title;
if (getComputedStyle(div).display === 'none') {
document.addEventListener('keydown', closeHelp);
window.addEventListener('keydown', closeHelp, true);
// avoid chaining on multiple showHelp() calls
$('.dismiss', div).onclick = closeHelp;
}
div.style.display = 'block';
// reset any inline styles
div.style = 'display: block';
return div;
function closeHelp(e) {
if (
!e ||
e.type === 'click' ||
((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
) {
if (!e || e.type === 'click' ||
(e.which === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey &&
!$('.CodeMirror-hints, #message-box') && !(document.activeElement instanceof HTMLInputElement))) {
if (e && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
messageBox.confirm(t('confirmDiscardChanges')).then(ok => ok && closeHelp());
return;
}
div.style.display = '';
const contents = $('.contents');
contents.textContent = '';
clearTimeout(contents.timer);
document.removeEventListener('keydown', closeHelp);
window.removeEventListener('keydown', closeHelp, true);
window.dispatchEvent(new Event('closeHelp'));
(editors.lastActive || editors[0]).focus();
}
}
}
@ -1909,46 +1935,20 @@ function showCodeMirrorPopup(title, html, options) {
chrome.runtime.onMessage.addListener(onRuntimeMessage);
function replaceStyle(request) {
const codeIsUpdated = request.codeIsUpdated !== false;
if (!isUsercss(request.style)) {
initWithSectionStyle(request);
return;
}
if (!codeIsUpdated) {
editor.replaceMeta(request.style);
return;
}
askDiscardChanges()
.then(result => {
if (result) {
editor.replaceStyle(request.style);
} else {
editor.setStyleDirty(request.style);
}
});
function askDiscardChanges() {
if (!editor.isTouched()) {
return Promise.resolve(true);
}
return messageBox.confirm(t('styleUpdateDiscardChanges'));
}
}
function onRuntimeMessage(request) {
switch (request.method) {
case 'styleUpdated':
if (styleId && styleId === request.style.id && request.reason !== 'editSave' && request.reason !== 'config') {
if (styleId && styleId === request.style.id &&
request.reason !== 'editSave' &&
request.reason !== 'config') {
// code-less style from notifyAllTabs
if ((request.style.sections[0] || {}).code === null) {
// the code-less style came from notifyAllTabs
onBackgroundReady().then(() => {
request.style = BG.cachedStyles.byId.get(request.style.id);
replaceStyle(request);
});
request.style = BG.cachedStyles.byId.get(request.style.id);
}
if (isUsercss(request.style)) {
editor.replaceStyle(request.style, request.codeIsUpdated);
} else {
replaceStyle(request);
initWithSectionStyle(request);
}
}
break;

View File

@ -1,34 +1,43 @@
/* global CodeMirror CSSLint stylelint linterConfig */
/* global CodeMirror linterConfig */
'use strict';
CodeMirror.registerHelper('lint', 'csslint', code =>
CSSLint.verify(code, deepCopy(linterConfig.getCurrent('csslint')))
.messages.map(message => ({
from: CodeMirror.Pos(message.line - 1, message.col - 1),
to: CodeMirror.Pos(message.line - 1, message.col),
message: message.message,
rule: message.rule.id,
severity : message.type
}))
);
(() => {
CodeMirror.registerHelper('lint', 'csslint', invokeHelper);
CodeMirror.registerHelper('lint', 'stylelint', invokeHelper);
CodeMirror.registerHelper('lint', 'stylelint', code =>
stylelint.lint({
code,
config: deepCopy(linterConfig.getCurrent('stylelint')),
}).then(({results}) => {
if (!results[0]) {
return [];
}
return results[0].warnings.map(warning => ({
from: CodeMirror.Pos(warning.line - 1, warning.column - 1),
to: CodeMirror.Pos(warning.line - 1, warning.column),
message: warning.text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase())
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
rule: warning.text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
severity : warning.severity
}));
})
);
const cookResults = {
csslint: results =>
results.map(({line, col: ch, message, rule, type: severity}) => line && {
message,
from: {line: line - 1, ch: ch - 1},
to: {line: line - 1, ch},
rule: rule.id,
severity,
}).filter(Boolean),
stylelint: ({results}) =>
!results[0] && [] ||
results[0].warnings.map(({line, column: ch, text, severity}) => ({
from: {line: line - 1, ch: ch - 1},
to: {line: line - 1, ch},
message: text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase())
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
severity,
})),
};
function invokeHelper(code, options, cm) {
const config = linterConfig.getCurrent();
return linterConfig.invokeWorker({code, config})
.then(cookResults[linterConfig.getName()])
.then(results => {
if (options && typeof options.preUpdateLinting === 'function') {
options.preUpdateLinting(cm);
}
return results;
});
}
})();

View File

@ -1,5 +1,5 @@
/* global CodeMirror messageBox */
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
/* global editors makeSectionVisible showCodeMirrorPopup showHelp hotkeyRerouter */
/* global loadScript require CSSLint stylelint */
/* global makeLink */
'use strict';
@ -20,28 +20,41 @@ var linterConfig = {
csslint: 'editorCSSLintConfig',
stylelint: 'editorStylelintConfig',
},
getDefault() {
// some dirty hacks to override editor.linter getting from prefs
const linter = prefs.get('editor.linter');
if (linter && editors[0] && editors[0].getOption('mode') !== 'css') {
return 'stylelint';
}
return linter;
worker: {
csslint: {path: '/vendor-overwrites/csslint/csslint-worker.js'},
stylelint: {path: '/vendor-overwrites/stylelint/stylelint-bundle.min.js'},
},
allRuleIds: {
csslint: null,
stylelint: null,
},
getCurrent(linter = linterConfig.getDefault()) {
getName() {
// some dirty hacks to override editor.linter getting from prefs
const linter = prefs.get('editor.linter');
const mode = linter && editors[0] && editors[0].doc.mode;
return mode && mode !== 'css' && mode.name !== 'css' ? 'stylelint' : linter;
},
getCurrent(linter = linterConfig.getName()) {
return this.fallbackToDefaults(this[linter] || {});
},
getForCodeMirror(linter = linterConfig.getDefault()) {
getForCodeMirror(linter = linterConfig.getName()) {
return CodeMirror.lint && CodeMirror.lint[linter] ? {
getAnnotations: CodeMirror.lint[linter],
delay: prefs.get('editor.lintDelay'),
preUpdateLinting(cm) {
cm.startOperation();
},
onUpdateLinting(annotationsNotSorted, annotations, cm) {
cm.endOperation();
updateLintReport(cm, 0);
},
} : false;
},
fallbackToDefaults(config, linter = linterConfig.getDefault()) {
fallbackToDefaults(config, linter = linterConfig.getName()) {
if (config && Object.keys(config).length) {
if (linter === 'stylelint') {
// always use default syntax because we don't expose it in config UI
@ -53,27 +66,52 @@ var linterConfig = {
}
},
setLinter(linter = linterConfig.getDefault()) {
setLinter(linter = linterConfig.getName()) {
linter = linter.toLowerCase();
linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
if (linterConfig.getDefault() !== linter) {
if (linterConfig.getName() !== linter) {
prefs.set('editor.linter', linter);
}
return linter;
},
findInvalidRules(config, linter = linterConfig.getDefault()) {
const rules = linter === 'stylelint' ? config.rules : config;
const allRules = new Set(
linter === 'stylelint'
? Object.keys(stylelint.rules)
: CSSLint.getRules().map(rule => rule.id)
invokeWorker(message) {
const worker = linterConfig.worker[message.linter || linterConfig.getName()];
if (!worker.queue) {
worker.queue = [];
worker.instance.onmessage = ({data}) => {
worker.queue.shift().resolve(data);
if (worker.queue.length) {
worker.instance.postMessage(worker.queue[0].message);
}
};
}
return new Promise(resolve => {
worker.queue.push({message, resolve});
if (worker.queue.length === 1) {
worker.instance.postMessage(message);
}
});
},
getAllRuleIds(linter = linterConfig.getName()) {
return Promise.resolve(
this.allRuleIds[linter] ||
this.invokeWorker({linter, action: 'getAllRuleIds'})
.then(ids => (this.allRuleIds[linter] = ids.sort()))
);
return Object.keys(rules).filter(rule => !allRules.has(rule));
},
findInvalidRules(config, linter = linterConfig.getName()) {
return this.getAllRuleIds(linter).then(allRuleIds => {
const allRuleIdsSet = new Set(allRuleIds);
const rules = linter === 'stylelint' ? config.rules : config;
return Object.keys(rules).filter(rule => !allRuleIdsSet.has(rule));
});
},
stringify(config = this.getCurrent()) {
if (linterConfig.getDefault() === 'stylelint') {
if (linterConfig.getName() === 'stylelint') {
config.syntax = undefined;
}
return JSON.stringify(config, null, 2)
@ -82,7 +120,7 @@ var linterConfig = {
save(config) {
config = this.fallbackToDefaults(config);
const linter = linterConfig.getDefault();
const linter = linterConfig.getName();
this[linter] = config;
BG.chromeSync.setLZValue(this.storageName[linter], config);
return config;
@ -140,19 +178,13 @@ function initLint() {
$('#lint-help').addEventListener('click', showLintHelp);
$('#lint').addEventListener('click', gotoLintIssue);
$('#linter-settings').addEventListener('click', linterConfig.openOnClick);
window.addEventListener('resize', resizeLintReport);
// touch devices don't have onHover events so the element we'll be toggled via clicking (touching)
if ('ontouchstart' in document.body) {
$('#lint h2').addEventListener('click', toggleLintReport);
}
updateLinter();
linterConfig.watchStorage();
prefs.subscribe(['editor.linter'], updateLinter);
}
function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
function updateLinter({immediately, linter = linterConfig.getName()} = {}) {
if (!immediately) {
debounce(updateLinter, 0, {immediately: true, linter});
return;
@ -164,13 +196,15 @@ function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
loadLinterAssets(linter)
]).then(updateEditors);
$('#linter-settings').style.display = !linter ? 'none' : 'inline-block';
$('#lint').style.display = 'none';
$('#lint').classList.add('hidden');
function updateEditors() {
CodeMirror.defaults.lint = linterConfig.getForCodeMirror(linter);
const guttersOption = prepareGuttersOption();
editors.forEach(cm => {
cm.setOption('lint', CodeMirror.defaults.lint);
if (cm.options.lint !== CodeMirror.defaults.lint) {
cm.setOption('lint', CodeMirror.defaults.lint);
}
if (guttersOption) {
cm.setOption('guttersOption', guttersOption);
updateGutters(cm, guttersOption);
@ -207,14 +241,6 @@ function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
}
function updateLintReport(cm, delay) {
if (cm && !cm.options.lint) {
// add 'lint' option back to the freshly created section
setTimeout(() => {
if (!cm.options.lint) {
cm.setOption('lint', linterConfig.getForCodeMirror());
}
});
}
const state = cm && cm.state && cm.state.lint || {};
if (delay === 0) {
// immediately show pending csslint/stylelint messages in onbeforeunload and save
@ -336,23 +362,7 @@ function renderLintReport(someBlockChanged) {
if (someBlockChanged || newContent.children.length !== content.children.length) {
$('#issue-count').textContent = issueCount;
container.replaceChild(newContent, content);
container.style.display = newContent.children.length ? 'block' : 'none';
resizeLintReport();
}
}
function resizeLintReport() {
// subtracted value to prevent scrollbar
const magicBuffer = 20;
const content = $('#lint table');
if (content) {
const bounds = content.getBoundingClientRect();
const newMaxHeight = bounds.bottom <= window.innerHeight ? '' :
// subtract out a bit of padding or the vertical scrollbar extends beyond the viewport
(window.innerHeight - bounds.top - magicBuffer) + 'px';
if (newMaxHeight !== content.style.maxHeight) {
content.parentNode.style.maxHeight = newMaxHeight;
}
container.classList.toggle('hidden', !newContent.children.length);
}
}
@ -370,22 +380,17 @@ function gotoLintIssue(event) {
});
}
function toggleLintReport() {
$('#lint').classList.toggle('collapsed');
}
function showLintHelp() {
const linter = linterConfig.getDefault();
const linter = linterConfig.getName();
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') {
const CSSLintRules = CSSLint.getRules();
headerLink = makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint');
template = ruleID => {
const rule = CSSLintRules.find(rule => rule.id === ruleID);
const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID);
return rule &&
$element({tag: 'li', appendChild: [
$element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}),
@ -415,148 +420,201 @@ function showLintHelp() {
);
}
function showLinterErrorMessage(title, contents) {
function showLinterErrorMessage(title, contents, popup) {
messageBox({
title,
contents,
className: 'danger center lint-config',
buttons: [t('confirmOK')],
});
}
function setupLinterSettingsEvents(popup) {
$('.save', popup).addEventListener('click', event => {
event.preventDefault();
const linter = linterConfig.setLinter(event.target.dataset.linter);
const json = tryJSONparse(popup.codebox.getValue());
if (json) {
const invalid = linterConfig.findInvalidRules(json, linter);
if (invalid.length) {
showLinterErrorMessage(linter, [
t('linterInvalidConfigError'),
$element({
tag: 'ul',
appendChild: invalid.map(name =>
$element({tag: 'li', textContent: name})),
}),
]);
return;
}
linterConfig.save(json);
linterConfig.showSavedMessage();
popup.codebox.markClean();
} else {
showLinterErrorMessage(linter, t('linterJSONError'));
}
popup.codebox.focus();
});
$('.reset', popup).addEventListener('click', event => {
event.preventDefault();
const linter = linterConfig.setLinter(event.target.dataset.linter);
popup.codebox.setValue(linterConfig.stringify(linterConfig.defaults[linter] || {}));
popup.codebox.focus();
});
$('.cancel', popup).addEventListener('click', event => {
event.preventDefault();
$('.dismiss').dispatchEvent(new Event('click'));
});
}).then(() => popup && popup.codebox && popup.codebox.focus());
}
function setupLinterPopup(config) {
const linter = linterConfig.getDefault();
const linter = linterConfig.getName();
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
function makeButton(className, text, options = {}) {
return $element(Object.assign(options, {
tag: 'button',
className,
type: 'button',
textContent: t(text),
dataset: {linter}
}));
}
function makeLink(url, textContent) {
return $element({tag: 'a', target: '_blank', href: url, textContent});
}
const defaultConfig = linterConfig.stringify(linterConfig.defaults[linter] || {});
const title = t('linterConfigPopupTitle', linterTitle);
const contents = $element({
appendChild: [
$element({
tag: 'p',
appendChild: [
t('linterRulesLink') + ' ',
makeLink(
linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
linterTitle
),
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : ''
]
}),
makeButton('save', 'styleSaveLabel', {disabled: true}),
makeButton('cancel', 'confirmCancel'),
makeButton('reset', 'genericResetLabel', {title: t('linterResetMessage')}),
$element({
tag: 'span',
className: 'saved-message',
textContent: t('genericSavedMessage')
})
]
const popup = showCodeMirrorPopup(title, null, {
lint: false,
extraKeys: {'Ctrl-Enter': save},
hintOptions: {hint},
});
const popup = showCodeMirrorPopup(title, contents, {lint: false});
contents.parentNode.appendChild(contents);
popup.codebox.focus();
popup.codebox.setValue(config);
popup.codebox.clearHistory();
popup.codebox.markClean();
popup.codebox.on('change', cm => {
$('.save', popup).disabled = cm.isClean();
$('.contents', popup).appendChild(makeFooter());
const cm = popup.codebox;
cm.focus();
cm.setValue(config);
cm.clearHistory();
cm.markClean();
cm.on('changes', updateButtonState);
updateButtonState();
hotkeyRerouter.setState(false);
window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _);
hotkeyRerouter.setState(true);
});
setupLinterSettingsEvents(popup);
loadScript([
'/vendor/codemirror/mode/javascript/javascript.js',
'/vendor/codemirror/addon/lint/json-lint.js',
'/vendor/jsonlint/jsonlint.js'
]).then(() => {
popup.codebox.setOption('mode', 'application/json');
popup.codebox.setOption('lint', 'json');
cm.setOption('mode', 'application/json');
cm.setOption('lint', 'json');
});
}
function loadLinterAssets(name = linterConfig.getDefault()) {
if (!name) {
return Promise.resolve();
}
return loadLibrary().then(loadAddon);
function loadLibrary() {
if (name === 'csslint' && !window.CSSLint) {
return loadScript([
'/vendor-overwrites/csslint/csslint-worker.js',
'/edit/lint-defaults-csslint.js'
]);
}
if (name === 'stylelint' && !window.stylelint) {
return loadScript([
'/vendor-overwrites/stylelint/stylelint-bundle.min.js',
'/edit/lint-defaults-stylelint.js'
]).then(() => (window.stylelint = require('stylelint')));
}
return Promise.resolve();
function makeFooter() {
const makeButton = (className, onclick, text, options = {}) =>
$element(Object.assign(options, {
className,
onclick,
tag: 'button',
type: 'button',
textContent: t(text),
}));
return $element({
appendChild: [
$element({
tag: 'p',
appendChild: [
t('linterRulesLink') + ' ',
$element({
tag: 'a',
target: '_blank',
href: linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
textContent: linterTitle
}),
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : ''
]
}),
makeButton('save', save, 'styleSaveLabel', {title: 'Ctrl-Enter'}),
makeButton('cancel', cancel, 'confirmClose'),
makeButton('reset', reset, 'genericResetLabel', {title: t('linterResetMessage')}),
$element({
tag: 'span',
className: 'saved-message',
textContent: t('genericSavedMessage')
})
]
});
}
function loadAddon() {
if (CodeMirror.lint) {
function save(event) {
if (event instanceof Event) {
event.preventDefault();
}
const json = tryJSONparse(cm.getValue());
if (!json) {
showLinterErrorMessage(linter, t('linterJSONError'), popup);
cm.focus();
return;
}
return loadScript([
linterConfig.findInvalidRules(json, linter).then(invalid => {
if (invalid.length) {
showLinterErrorMessage(linter, [
t('linterInvalidConfigError'),
$element({tag: 'ul', appendChild: invalid.map(name =>
$element({tag: 'li', textContent: name})),
}),
], popup);
return;
}
linterConfig.setLinter(linter);
linterConfig.save(json);
linterConfig.showSavedMessage();
cm.markClean();
cm.focus();
updateButtonState();
});
}
function reset(event) {
event.preventDefault();
linterConfig.setLinter(linter);
cm.setValue(defaultConfig);
cm.focus();
updateButtonState();
}
function cancel(event) {
event.preventDefault();
$('.dismiss').dispatchEvent(new Event('click'));
}
function updateButtonState() {
$('.save', popup).disabled = cm.isClean();
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
}
function hint(cm) {
return Promise.all([
linterConfig.getAllRuleIds(linter),
linter !== 'stylelint' || hint.allOptions ||
linterConfig.invokeWorker({action: 'getAllRuleOptions', linter})
.then(options => (hint.allOptions = options)),
])
.then(([ruleIds, options]) => {
const cursor = cm.getCursor();
const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
const {line, ch} = cursor;
const quoted = string.startsWith('"');
const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
const depth = getLexicalDepth(lexical);
const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
let [, prevWord] = search.find(true) || [];
let words = [];
if (depth === 1 && linter === 'stylelint') {
words = quoted ? ['rules'] : [];
} else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
words = ruleIds;
} else if (depth === 2 || depth === 3 && lexical.type === ']') {
words = !quoted ? ['true', 'false', 'null'] :
ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
} else if (depth === 4 && prevWord === 'severity') {
words = ['error', 'warning'];
} else if (depth === 4) {
words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
} else if (depth === 5 && lexical.type === ']' && quoted) {
while (prevWord && !ruleIds.includes(prevWord)) {
prevWord = (search.find(true) || [])[1];
}
words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
}
return {
list: words.filter(word => word.startsWith(leftPart)),
from: {line, ch: start + (quoted ? 1 : 0)},
to: {line, ch: string.endsWith('"') ? end - 1 : end},
};
});
}
function getLexicalDepth(lexicalState) {
let depth = 0;
while ((lexicalState = lexicalState.prev)) {
depth++;
}
return depth;
}
}
function loadLinterAssets(name = linterConfig.getName()) {
const worker = linterConfig.worker[name];
return !name || !worker || worker.instance ? Promise.resolve() :
loadScript((worker.instance ? [] : [
(worker.instance = new Worker(worker.path)),
`/edit/lint-defaults-${name}.js`,
]).concat(CodeMirror.lint ? [] : [
'/vendor/codemirror/addon/lint/lint.css',
'/msgbox/msgbox.css',
'/vendor/codemirror/addon/lint/lint.js',
'/edit/lint-codemirror-helper.js',
'/msgbox/msgbox.js'
]);
}
]));
}

View File

@ -0,0 +1,200 @@
/* global CodeMirror */
'use strict';
(() => {
/*
The original match-highlighter addon always recreates the highlight overlay
even if the token under cursor hasn't changed, which is terribly ineffective
(the entire view is re-rendered) and makes our animated token highlight effect
restart on every cursor movement.
Invocation sequence of our hooks:
1. removeOverlayForHighlighter()
The original addon removes the overlay unconditionally
so this hook saves the state if the token hasn't changed.
2. addOverlayForHighlighter()
Restores the saved state instead of creating a new overlay,
installs a hook to count occurrences.
3. matchesOnScrollbar()
Saves the query regexp passed from the original addon in our helper object,
and in case removeOverlayForHighlighter() decided to keep the overlay
only rewrites the regexp without invoking the original constructor.
*/
const HL_APPROVED = 'cm-matchhighlight-approved';
const originalAddOverlay = CodeMirror.prototype.addOverlay;
const originalRemoveOverlay = CodeMirror.prototype.removeOverlay;
const originalMatchesOnScrollbar = CodeMirror.prototype.showMatchesOnScrollbar;
let originalGetOption;
CodeMirror.prototype.addOverlay = addOverlay;
CodeMirror.prototype.removeOverlay = removeOverlay;
CodeMirror.prototype.showMatchesOnScrollbar = matchesOnScrollbar;
return;
function shouldIntercept(overlay) {
const hlState = this.state.matchHighlighter || {};
return overlay === hlState.overlay && (hlState.options || {}).showToken;
}
function addOverlay() {
return shouldIntercept.apply(this, arguments) &&
addOverlayForHighlighter.apply(this, arguments) ||
originalAddOverlay.apply(this, arguments);
}
function removeOverlay() {
return shouldIntercept.apply(this, arguments) &&
removeOverlayForHighlighter.apply(this, arguments) ||
originalRemoveOverlay.apply(this, arguments);
}
function addOverlayForHighlighter(overlay) {
const state = this.state.matchHighlighter || {};
const helper = state.highlightHelper = state.highlightHelper || {};
helper.rewriteScrollbarQuery = true;
// since we're here the original addon decided there's something to highlight,
// so we cancel removeOverlayIfExpired() scheduled in our removeOverlay hook
clearTimeout(helper.hookTimer);
// the original addon just removed its overlays, which was intercepted by removeOverlayForHighlighter,
// which decided to restore it and saved the previous overlays in our helper object,
// so here we are now, restoring them
if (helper.skipMatchesOnScrollbar) {
state.matchesonscroll = helper.matchesonscroll;
state.overlay = helper.overlay;
return true;
}
// hook the newly created overlay's token() to count the occurrences
if (overlay.token !== tokenHook) {
overlay.highlightHelper = {
token: overlay.token,
occurrences: 0,
};
overlay.token = tokenHook;
}
// speed up rendering of scrollbar marks 4 times: we don't need ultimate precision there
// so for the duration of this event loop cycle we spoof the "lineWrapping" option
// and restore it in the next event loop cycle
if (this.options.lineWrapping && CodeMirror.prototype.getOption !== spoofLineWrappingOption) {
originalGetOption = CodeMirror.prototype.getOption;
CodeMirror.prototype.getOption = spoofLineWrappingOption;
setTimeout(() => (CodeMirror.prototype.getOption = originalGetOption));
}
}
function spoofLineWrappingOption(option) {
return option !== 'lineWrapping' && originalGetOption.apply(this, arguments);
}
function tokenHook(stream) {
// we don't highlight a single match in case 'editor.matchHighlight' option is 'token'
// so this hook counts the occurrences and toggles HL_APPROVED class on CM's wrapper element
const style = this.highlightHelper.token.call(this, stream);
if (style !== 'matchhighlight') {
return style;
}
const num = ++this.highlightHelper.occurrences;
if (num === 1) {
stream.lineOracle.doc.cm.display.wrapper.classList.remove(HL_APPROVED);
} else if (num === 2) {
stream.lineOracle.doc.cm.display.wrapper.classList.add(HL_APPROVED);
}
return style;
}
function removeOverlayForHighlighter() {
const state = this.state.matchHighlighter || {};
const helper = state.highlightHelper;
const {query, originalToken} = helper || state.matchesonscroll || {};
// no current query means nothing to preserve => remove the overlay
if (!query || !originalToken) {
return;
}
const rx = query instanceof RegExp && query;
const sel = this.getSelection();
// current query differs from the selected text => remove the overlay
if (sel && (rx && !rx.test(sel) || sel.toLowerCase() !== query)) {
return;
}
// if token under cursor has changed => remove the overlay
if (!sel) {
const {line, ch} = this.getCursor();
const queryLen = originalToken.length;
const start = Math.max(0, ch - queryLen + 1);
const end = ch + queryLen;
const string = this.getLine(line);
const area = string.slice(start, end);
let startInArea;
if (rx) {
const m = area.match(rx);
startInArea = !m ? NaN : m.index + m[1].length;
} else {
const i = area.indexOf(query);
startInArea = i < 0 ? NaN : i;
}
if (isNaN(startInArea) || start + startInArea > ch ||
state.options.showToken.test(string[start + startInArea - 1] || '') ||
state.options.showToken.test(string[start + startInArea + queryLen] || '')) {
// pass the displayed instance back to the original code to remove it
state.matchesonscroll = state.matchesonscroll || helper && helper.matchesonscroll;
return;
}
}
// since the same token is under cursor we prevent the highlighter from rerunning
// by saving current overlays in a helper object so that it's restored in addOverlayForHighlighter()
state.highlightHelper = {
overlay: state.overlay,
matchesonscroll: state.matchesonscroll || (helper || {}).matchesonscroll,
// instruct our matchesOnScrollbar hook to preserve current scrollbar annotations
skipMatchesOnScrollbar: true,
// in case the original addon won't highlight anything we need to actually remove the overlays
// by setting a timer that runs in the next event loop cycle and can be canceled in this cycle
hookTimer: setTimeout(removeOverlayIfExpired, 0, this, state),
};
// fool the original addon so it won't invoke state.matchesonscroll.clear()
state.matchesonscroll = null;
return true;
}
function removeOverlayIfExpired(self, state) {
const {overlay, matchesonscroll} = state.highlightHelper || {};
if (overlay) {
originalRemoveOverlay.call(self, overlay);
}
if (matchesonscroll) {
matchesonscroll.clear();
}
state.highlightHelper = null;
}
function matchesOnScrollbar(query, ...args) {
const state = this.state.matchHighlighter;
const helper = state.highlightHelper = state.highlightHelper || {};
// rewrite the \btoken\b regexp so it matches .token and #token and --token
if (helper.rewriteScrollbarQuery && /^\\b.*?\\b$/.test(query.source)) {
helper.rewriteScrollbarQuery = undefined;
helper.originalToken = query.source.slice(2, -2);
const notToken = '(?!' + state.options.showToken.source + ').';
query = new RegExp(`(^|${notToken})` + helper.originalToken + `(${notToken}|$)`);
}
// save the query for future use in removeOverlayForHighlighter
helper.query = query;
// if removeOverlayForHighlighter() decided to keep the overlay
if (helper.skipMatchesOnScrollbar) {
helper.skipMatchesOnScrollbar = undefined;
return;
} else {
return originalMatchesOnScrollbar.call(this, query, ...args);
}
}
})();

View File

@ -1,6 +1,6 @@
/* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
/* global hotkeyRerouter setupAutocomplete setupOptionsExpand */
/* global hotkeyRerouter setupAutocomplete */
/* global editors linterConfig updateLinter regExpTester mozParser */
/* global makeLink createAppliesToLineWidget messageBox */
'use strict';
@ -8,29 +8,19 @@
function createSourceEditor(style) {
// a flag for isTouched()
let hadBeenSaved = false;
let savedGeneration = 0;
$('#sections').textContent = '';
$('#name').disabled = true;
$('#mozilla-format-heading').parentNode.remove();
$('#mozilla-format-container').remove();
$('#sections').textContent = '';
$('#sections').appendChild(
$element({className: 'single-editor', appendChild: [
$element({tag: 'textarea'})
]})
$element({className: 'single-editor'})
);
$('#header').appendChild($element({
id: 'footer',
appendChild: makeLink('https://github.com/openstyles/stylus/wiki/Usercss', t('externalUsercssDocument'))
}));
setupOptionsExpand();
const dirty = dirtyReporter();
dirty.onChange(() => {
const DIRTY = dirty.isDirty();
document.body.classList.toggle('dirty', DIRTY);
$('#save-button').disabled = !DIRTY;
document.body.classList.toggle('dirty', dirty.isDirty());
$('#save-button').disabled = !dirty.isDirty();
updateTitle();
});
@ -42,70 +32,51 @@ function createSourceEditor(style) {
style = deepCopy(style);
}
const cm = CodeMirror.fromTextArea($('#sections textarea'));
cm.startOperation();
cm.setValue(style.sourceCode);
cm.clearHistory();
cm.markClean();
const cm = CodeMirror($('.single-editor'));
editors.push(cm);
updateMeta();
cm.endOperation();
updateMeta().then(() => {
initLint();
initLinterSwitch();
initHooks();
initAppliesToLineWidget();
cm.setValue(style.sourceCode);
cm.clearHistory();
cm.markClean();
savedGeneration = cm.changeGeneration();
initLint();
initLinterSwitch();
initHooks();
initAppliesToLineWidget();
// focus must be the last action, otherwise the style is duplicated on saving
cm.focus();
// focus must be the last action, otherwise the style is duplicated on saving
cm.focus();
});
function initAppliesToLineWidget() {
const PREF_NAME = 'editor.appliesToLineWidget';
const widget = createAppliesToLineWidget(cm);
const optionEl = buildOption();
$('#options').insertBefore(optionEl, $('#options > .option.aligned'));
widget.toggle(prefs.get(PREF_NAME));
prefs.subscribe([PREF_NAME], (key, value) => {
widget.toggle(value);
optionEl.checked = value;
});
optionEl.addEventListener('change', e => {
prefs.set(PREF_NAME, e.target.checked);
});
function buildOption() {
return $element({className: 'option', appendChild: [
$element({
tag: 'input',
type: 'checkbox',
id: PREF_NAME,
checked: prefs.get(PREF_NAME)
}),
$element({
tag: 'label',
htmlFor: PREF_NAME,
textContent: ' ' + t('appliesLineWidgetLabel'),
title: t('appliesLineWidgetWarning')
})
]});
}
prefs.subscribe([PREF_NAME], (key, value) => widget.toggle(value));
}
function initLinterSwitch() {
const linterEl = $('#editor.linter');
let prevMode = NaN;
cm.on('optionChange', (cm, option) => {
if (option !== 'mode') {
return;
}
const mode = cm.doc.mode;
if (mode === prevMode || mode && mode.name === prevMode) {
return;
}
prevMode = mode;
updateLinter();
update();
});
linterEl.addEventListener('change', update);
update();
function update() {
linterEl.value = linterConfig.getDefault();
linterEl.value = linterConfig.getName();
const cssLintOption = linterEl.querySelector('[value="csslint"]');
if (cm.getOption('mode') !== 'css') {
@ -126,56 +97,55 @@ function createSourceEditor(style) {
section = mozParser.format(style);
}
const sourceCode = `/* ==UserStyle==
@name New Style - ${Date.now()}
@namespace github.com/openstyles/stylus
@version 0.1.0
@description A new userstyle
@author Me
==/UserStyle== */
${section}
`;
dirty.modify('source', '', sourceCode);
style.sourceCode = sourceCode;
const DEFAULT_CODE = `
/* ==UserStyle==
@name ${t('usercssReplaceTemplateName') + ' - ' + new Date().toLocaleString()}
@namespace github.com/openstyles/stylus
@version 0.1.0
@description A new userstyle
@author Me
==/UserStyle== */
${section}
`.replace(/^\s+/gm, '');
dirty.clear('sourceGeneration');
style.sourceCode = '';
BG.chromeSync.getLZValue('usercssTemplate').then(code => {
style.sourceCode = code || DEFAULT_CODE;
cm.startOperation();
cm.setValue(style.sourceCode);
cm.clearHistory();
cm.markClean();
cm.endOperation();
dirty.clear('sourceGeneration');
savedGeneration = cm.changeGeneration();
});
}
function initHooks() {
// sidebar commands
$('#save-button').onclick = save;
$('#beautify').onclick = beautify;
$('#keyMap-help').onclick = showKeyMapHelp;
$('#toggle-style-help').onclick = showToggleStyleHelp;
$('#cancel-button').onclick = goBackToManage;
// enable
$('#enabled').onchange = e => {
const value = e.target.checked;
$('#enabled').onchange = function () {
const value = this.checked;
dirty.modify('enabled', style.enabled, value);
style.enabled = value;
};
// source
cm.on('change', () => {
const value = cm.getValue();
dirty.modify('source', style.sourceCode, value);
style.sourceCode = value;
cm.on('changes', () => {
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
updateLintReportIfEnabled(cm);
});
// hotkeyRerouter
cm.on('focus', () => {
hotkeyRerouter.setState(false);
});
cm.on('blur', () => {
hotkeyRerouter.setState(true);
});
cm.on('focus', () => hotkeyRerouter.setState(false));
cm.on('blur', () => hotkeyRerouter.setState(true));
// autocomplete
if (prefs.get('editor.autocompleteOnTyping')) {
setupAutocomplete(cm);
}
//if (prefs.get('editor.autocompleteOnTyping')) {
// setupAutocomplete(cm);
//}
}
function updateMeta() {
@ -183,40 +153,55 @@ ${section}
$('#enabled').checked = style.enabled;
$('#url').href = style.url;
const {usercssData: {preprocessor} = {}} = style;
cm.setPreprocessor(preprocessor);
// beautify only works with regular CSS
$('#beautify').disabled = cm.getOption('mode') !== 'css';
updateTitle();
return cm.setPreprocessor(preprocessor);
}
function updateTitle() {
// title depends on dirty and style meta
if (!style.id) {
document.title = t('addStyleTitle');
} else {
document.title = (dirty.isDirty() ? '* ' : '') + t('editStyleTitle', [style.name]);
const newTitle = (dirty.isDirty() ? '* ' : '') +
(style.id ? t('editStyleTitle', [style.name]) : t('addStyleTitle'));
if (document.title !== newTitle) {
document.title = newTitle;
}
}
function replaceStyle(newStyle) {
if (!style.id && newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
function replaceStyle(newStyle, codeIsUpdated) {
const sameCode = newStyle.sourceCode === cm.getValue();
hadBeenSaved = sameCode;
if (sameCode) {
savedGeneration = cm.changeGeneration();
dirty.clear('sourceGeneration');
}
style = deepCopy(newStyle);
updateMeta();
if (style.sourceCode !== cm.getValue()) {
const cursor = cm.getCursor();
cm.setValue(style.sourceCode);
cm.setCursor(cursor);
if (codeIsUpdated === false || sameCode) {
updateEnvironment();
dirty.clear('enabled');
return;
}
dirty.clear();
hadBeenSaved = false;
}
function setStyleDirty(newStyle) {
dirty.clear();
dirty.modify('source', newStyle.sourceCode, style.sourceCode);
dirty.modify('enabled', newStyle.enabled, style.enabled);
Promise.resolve(messageBox.confirm(t('styleUpdateDiscardChanges'))).then(ok => {
if (!ok) {
return;
}
updateEnvironment();
if (!sameCode) {
const cursor = cm.getCursor();
cm.setValue(style.sourceCode);
cm.setCursor(cursor);
savedGeneration = cm.changeGeneration();
}
dirty.clear();
});
function updateEnvironment() {
if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
}
sessionStorage.justEditedStyleId = newStyle.id;
style = deepCopy(newStyle);
updateMeta();
}
}
function toggleStyle() {
@ -237,14 +222,25 @@ ${section}
reason: 'editSave',
id: style.id,
enabled: style.enabled,
sourceCode: style.sourceCode
sourceCode: cm.getValue(),
}))
.then(replaceStyle)
.then(() => {
hadBeenSaved = true;
})
.then(() => cm.setOption('mode', cm.doc.mode))
.catch(err => {
const contents = [String(err)];
if (err.message === t('styleMissingMeta', 'name')) {
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
BG.chromeSync.setLZValue('usercssTemplate', style.sourceCode)
.then(() => BG.chromeSync.getLZValue('usercssTemplate'))
.then(saved => {
if (saved !== style.sourceCode) {
messageBox.alert(t('syncStorageErrorSaving'));
}
}));
return;
}
const contents = Array.isArray(err) ?
$element({tag: 'pre', textContent: err.join('\n')}) :
[String(err)];
if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index);
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
@ -253,20 +249,26 @@ ${section}
textContent: drawLinePointer(pos)
}));
}
console.error(err);
messageBox.alert(contents);
});
function drawLinePointer(pos) {
const SIZE = 60;
const line = cm.getLine(pos.line);
const numTabs = pos.ch + 1 - line.slice(0, pos.ch + 1).replace(/\t/g, '').length;
const pointer = ' '.repeat(pos.ch) + '^';
const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0);
const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length);
const leftPad = start !== 0 ? '...' : '';
const rightPad = end !== line.length ? '...' : '';
return leftPad + line.slice(start, end) + rightPad + '\n' +
' '.repeat(leftPad.length) + pointer.slice(start, end);
return (
leftPad +
line.slice(start, end).replace(/\t/g, ' '.repeat(cm.options.tabSize)) +
rightPad +
'\n' +
' '.repeat(leftPad.length + numTabs * cm.options.tabSize) +
pointer.slice(start, end)
);
}
}
@ -275,16 +277,8 @@ ${section}
return dirty.isDirty() || hadBeenSaved;
}
function replaceMeta(newStyle) {
style.enabled = newStyle.enabled;
dirty.clear('enabled');
updateMeta();
}
return {
replaceStyle,
replaceMeta,
setStyleDirty,
save,
toggleStyle,
isDirty: dirty.isDirty,

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html id="stylus" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -12,26 +12,23 @@
<script src="/js/localization.js"></script>
<script src="/content/apply.js"></script>
<script src="/vendor/node-semver/semver.js"></script>
<script src="js/script-loader.js"></script>
<script src="/msgbox/msgbox.js"></script>
<link rel="stylesheet" href="/msgbox/msgbox.css"></script>
<link rel="stylesheet" href="/msgbox/msgbox.css">
<!-- FIXME: do we need all of these? -->
<script src="/vendor/codemirror/lib/codemirror.js"></script>
<script src="/vendor/codemirror/keymap/sublime.js"></script>
<script src="/vendor/codemirror/keymap/emacs.js"></script>
<script src="/vendor/codemirror/keymap/vim.js"></script>
<script src="/edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css">
<link rel="stylesheet" href="/vendor/codemirror/lib/codemirror.css">
<script src="/vendor/codemirror/mode/css/css.js"></script>
<link rel="stylesheet" href="/vendor/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/vendor/codemirror/addon/search/matchesonscrollbar.css">
<script src="/vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="/vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="/vendor-overwrites/codemirror/addon/search/match-highlighter.js"></script>
<script src="/vendor/codemirror/addon/search/match-highlighter.js"></script>
<script src="/vendor/codemirror/addon/dialog/dialog.js"></script>
<script src="/vendor/codemirror/addon/search/searchcursor.js"></script>
<script src="/vendor/codemirror/addon/search/search.js"></script>
@ -47,9 +44,12 @@
<link rel="stylesheet" href="/vendor/codemirror/addon/hint/show-hint.css" />
<script src="/vendor/codemirror/addon/hint/show-hint.js"></script>
<script src="/vendor/codemirror/addon/hint/css-hint.js"></script>
<script src="/vendor/codemirror/addon/mode/loadmode.js"></script>
<script src="/edit/match-highlighter-helper.js"></script>
<script src="/edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css">
</head>
<body>
<body id="stylus-install-usercss">
<div class="container">
<div class="header">
<h1>
@ -57,6 +57,7 @@
<small class="meta-version"></small>
</h1>
<div class="actions">
<h2 class="installed" i18n-text="installButtonInstalled"></h2>
<button class="install" i18n-text="installButton"></button>
<label class="set-update-url">
<input type="checkbox">
@ -82,12 +83,9 @@
<div class="external-link"></div>
</div>
<div class="main">
<div class="code">
<textarea></textarea>
</div>
<div class="warnings"></div>
</div>
</div>
<script src="/content/util.js"></script>
<script src="/install-usercss/install-usercss.js"></script>
</body>
</html>

View File

@ -4,10 +4,6 @@ body {
background: white;
}
* {
box-sizing: border-box;
}
a {
color: #000;
transition: color .5s;
@ -19,27 +15,81 @@ a:hover {
}
img.icon,
svg.icon {
.svg-icon {
height: 1.4em;
vertical-align: middle;
}
.svg-icon {
margin-left: 2px;
fill: #000;
transition: fill .5s;
}
a:hover .svg-icon {
fill: #666;
}
input:disabled + span {
color: rgb(128, 128, 128);
}
.container {
display: flex;
height: 100vh;
align-items: stretch;
}
.header {
.main {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.header,
.warnings {
flex: 0 0 280px;
box-sizing: border-box;
padding: 15px;
border-right: 1px dashed #aaa;
box-shadow: 0 0 50px -18px black;
overflow-wrap: break-word;
overflow: auto;
overflow-y: auto;
z-index: 100;
}
.header > :first-child {
.header.meta-init-error {
display: none;
}
.warnings {
display: none;
padding-bottom: 0;
flex-basis: auto;
background: #ffe2e2;
border-right: none;
border-bottom: 1px dashed #aaa;
}
.has-warnings .warnings {
display: initial;
}
.warning {
font-weight: bold;
font-size: 125%;
margin-bottom: 1em;
}
.warning pre {
overflow-wrap: break-word;
white-space: pre-wrap;
margin: 1ex 0 0;
font-weight: normal;
font-size: 80%;
}
h1 {
margin-top: 0;
}
@ -51,30 +101,89 @@ h1 small {
content: " v";
}
.warning {
padding: 3px 6px;
border: 1px dashed black;
border-color: #ef6969;
background: #ffe2e2;
}
.header .warning {
margin: 3px 0;
}
.actions {
margin: 15px 0;
margin-bottom: 1em;
}
.actions label {
max-width: fit-content;
max-width: -moz-fit-content;
max-width: fit-content;
display: flex;
align-items: center;
margin: 0.5em 0;
}
.install {
font-family: Arial, "DejaVu Sans", Verdana, Geneva, sans-serif;
font-size: 14px;
background: linear-gradient(#666, #555);
color: white;
border-radius: 4px;
padding: 4px 38px 4px 10px;
position: relative;
display: inline-block;
text-shadow: 1px 1px 1px #333;
-webkit-appearance: none;
-moz-appearance: none;
border-style: none;
margin-bottom: 1ex;
cursor: pointer;
box-shadow: inset 0 -1px 0 0 hsl(0, 0%, 24%), inset 0 1px 0 0 hsl(0, 0%, 30%), inset 1px 0 0 0 hsl(0, 0%, 24%);
}
.install:before,
.install:after {
content: "";
vertical-align: middle;
box-sizing: border-box;
border-radius: 0 4px 4px 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 30px;
}
.install:before {
background-image: linear-gradient(hsl(176, 44%, 40%), hsl(176, 50%, 35%));
box-shadow: inset 0 -1px 0 0 hsl(0, 0%, 24%), inset 0 1px 0 0 hsl(0, 0%, 30%), inset 1px 0 0 0 rgba(0, 0, 0, .3), inset -1px 0 0 0 hsl(0, 0%, 38%);
padding: 5px 8px;
}
.install:after {
background-image: url(../images/icon/16.png);
background-repeat: no-repeat;
background-position: center center;
}
.install:hover {
filter: brightness(1.1);
color: #eee;
text-shadow: none;
}
.install.reinstall:after {
background-color: #333;
filter: grayscale(100%);
}
.install.update:before,
.install.update:after {
filter: hue-rotate(-18deg) brightness(.7) contrast(2);
}
.install.installed,
h2.installed {
display: none;
}
h2.installed.active {
display: inline-block;
font-weight: bold;
margin-bottom: 1ex;
margin-top: 0;
}
.actions label input {
margin: 0 0.5em 0 0;
flex: 0 0 auto;
@ -92,30 +201,306 @@ h1 small {
margin: 0 7.5px;
}
.code {
padding: 2em;
li {
margin-left: -2em;
}
.main {
flex: 1 1 auto;
overflow: hidden;
overflow-wrap: break-word;
min-width: 0;
display: flex;
flex-direction: column;
}
.main > :first-child {
flex: 0 0 auto;
}
.main > :last-child {
flex: 1 1 auto;
min-height: 0;
}
.main .code,
.main .CodeMirror {
height: 100%;
height: auto;
border: none;
}
/* spinner-related */
.header {
position: relative;
}
.header:not(.meta-init) > *:not(.lds-spinner),
.header.meta-init > .lds-spinner {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
opacity: 0;
}
.header.meta-init > * {
opacity: 1;
transition: opacity .5s;
-webkit-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
user-select: auto;
}
/* spinner: https://github.com/loadingio/css-spinner */
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-webkit-keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.lds-spinner {
position: absolute;
width: 200px;
height: 200px;
top: 50px;
left: 0;
right: 0;
margin: auto;
opacity: .2;
transition: opacity .5s;
}
.lds-spinner div {
left: 94px;
top: 23px;
position: absolute;
-webkit-animation: lds-spinner linear 1s infinite;
animation: lds-spinner linear 1s infinite;
background: currentColor;
width: 12px;
height: 34px;
border-radius: 20%;
-webkit-transform-origin: 6px 77px;
transform-origin: 6px 77px;
}
.lds-spinner div:nth-child(1) {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
-webkit-animation-delay: -0.916666666666667s;
animation-delay: -0.916666666666667s;
}
.lds-spinner div:nth-child(2) {
-webkit-transform: rotate(30deg);
transform: rotate(30deg);
-webkit-animation-delay: -0.833333333333333s;
animation-delay: -0.833333333333333s;
}
.lds-spinner div:nth-child(3) {
-webkit-transform: rotate(60deg);
transform: rotate(60deg);
-webkit-animation-delay: -0.75s;
animation-delay: -0.75s;
}
.lds-spinner div:nth-child(4) {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-animation-delay: -0.666666666666667s;
animation-delay: -0.666666666666667s;
}
.lds-spinner div:nth-child(5) {
-webkit-transform: rotate(120deg);
transform: rotate(120deg);
-webkit-animation-delay: -0.583333333333333s;
animation-delay: -0.583333333333333s;
}
.lds-spinner div:nth-child(6) {
-webkit-transform: rotate(150deg);
transform: rotate(150deg);
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(7) {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
-webkit-animation-delay: -0.416666666666667s;
animation-delay: -0.416666666666667s;
}
.lds-spinner div:nth-child(8) {
-webkit-transform: rotate(210deg);
transform: rotate(210deg);
-webkit-animation-delay: -0.333333333333333s;
animation-delay: -0.333333333333333s;
}
.lds-spinner div:nth-child(9) {
-webkit-transform: rotate(240deg);
transform: rotate(240deg);
-webkit-animation-delay: -0.25s;
animation-delay: -0.25s;
}
.lds-spinner div:nth-child(10) {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
-webkit-animation-delay: -0.166666666666667s;
animation-delay: -0.166666666666667s;
}
.lds-spinner div:nth-child(11) {
-webkit-transform: rotate(300deg);
transform: rotate(300deg);
-webkit-animation-delay: -0.083333333333333s;
animation-delay: -0.083333333333333s;
}
.lds-spinner div:nth-child(12) {
-webkit-transform: rotate(330deg);
transform: rotate(330deg);
-webkit-animation-delay: 0s;
animation-delay: 0s;
}
/* https://github.com/lukehaas/css-loaders */
/*
.spinner {
--color: currentColor;
--background-color: currentColor;
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 200px;
height: 200px;
border-radius: 50%;
background: #ffffff;
background: -moz-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: -webkit-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: -ms-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
background: linear-gradient(to right, #ffffff 10%, rgba(255, 255, 255, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before {
width: 50%;
height: 50%;
background: #ffffff;
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
.spinner:after {
background: #0dc5c1;
width: 75%;
height: 75%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
*/
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/************ reponsive layouts ************/
@media (max-width:10in) {
.container {
flex-direction: column;
}
.header {
flex-basis: auto;
border-right: none;
border-bottom: 1px dashed #AAA;
max-height: 50%;
overflow-x: auto;
overflow-y: hidden;
}
.has-warnings .header {
min-height: 4em;
max-height: 20%;
}
.warnings {
max-height: 20%;
}
.warning:not(:last-child) {
border-bottom: 1px dashed #b57c7c;
padding-bottom: 1em;
}
.header,
.warning {
-webkit-column-count: 3;
-moz-column-count: 3;
column-count: 3;
}
h1 {
-webkit-column-span: all;
column-span: all;
margin-bottom: .5em;
}
.actions {
display: flex;
}
.install {
margin-right: 1em;
}
}
@media (max-width:7in) {
.header,
.warning {
-webkit-column-count: 2;
-moz-column-count: 2;
column-count: 2;
}
}
@media (max-width:4in) {
.header {
overflow-x: hidden;
overflow-y: auto;
max-height: 100%;
}
.header,
.warning {
-webkit-column-count: 1;
-moz-column-count: 1;
column-count: 1;
}
.warning {
border: none;
padding-bottom: unset;
}
}
/* Retina-specific stuff here */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.install:after {
background-image: url(../images/icon/32.png);
background-size: 16px 16px;
}
}

View File

@ -1,39 +1,56 @@
/* global CodeMirror semverCompare makeLink closeCurrentTab runtimeSend */
/* global messageBox */
/* global CodeMirror semverCompare makeLink closeCurrentTab */
/* global messageBox download chromeLocal */
'use strict';
(() => {
const params = new URLSearchParams(location.search);
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
let liveReload = false;
let installed = false;
const port = chrome.tabs.connect(
Number(params.get('tabId')),
{name: 'usercss-install', frameId: 0}
);
port.postMessage({method: 'getSourceCode'});
port.onMessage.addListener(msg => {
switch (msg.method) {
case 'getSourceCodeResponse':
if (msg.error) {
messageBox.alert(msg.error);
} else {
initSourceCode(msg.sourceCode);
}
break;
case 'sourceCodeChanged':
if (msg.error) {
messageBox.alert(msg.error);
} else {
liveReloadUpdate(msg.sourceCode);
}
break;
}
});
port.onDisconnect.addListener(closeCurrentTab);
const tabId = Number(params.get('tabId'));
let port;
const cm = CodeMirror.fromTextArea($('.code textarea'), {readOnly: true});
if (params.has('direct')) {
$('.live-reload').remove();
getCodeDirectly();
} else {
port = chrome.tabs.connect(tabId);
port.postMessage({method: 'getSourceCode'});
port.onMessage.addListener(msg => {
switch (msg.method) {
case 'getSourceCodeResponse':
if (msg.error) {
messageBox.alert(msg.error);
} else {
initSourceCode(msg.sourceCode);
}
break;
case 'sourceCodeChanged':
if (msg.error) {
messageBox.alert(msg.error);
} else {
liveReloadUpdate(msg.sourceCode);
}
break;
}
});
port.onDisconnect.addListener(closeCurrentTab);
}
const cm = CodeMirror($('.main'), {readOnly: true});
let liveReloadPending = Promise.resolve();
window.addEventListener('resize', adjustCodeHeight);
setTimeout(() => {
if (!installed) {
const div = $element({});
$('.header').appendChild($element({
className: 'lds-spinner',
appendChild: new Array(12).fill(div).map(e => e.cloneNode()),
}));
}
}, 200);
function liveReloadUpdate(sourceCode) {
liveReloadPending = liveReloadPending.then(() => {
@ -43,7 +60,7 @@
cm.setCursor(cursor);
cm.scrollTo(scrollInfo.left, scrollInfo.top);
return runtimeSend({
return sendMessage({
id: installed.id,
method: 'saveUsercss',
reason: 'update',
@ -54,8 +71,6 @@
}
function updateMeta(style, dup) {
$$('.main .warning').forEach(e => e.remove());
const data = style.usercssData;
const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version);
@ -67,6 +82,7 @@
document.title = `${installButtonLabel()} ${data.name}`;
$('.install').textContent = installButtonLabel();
$('.install').classList.add(installButtonClass());
$('.set-update-url').title = dup && dup.updateUrl && t('installUpdateFrom', dup.updateUrl) || '';
$('.meta-name').textContent = data.name;
$('.meta-version').textContent = data.version;
@ -94,6 +110,13 @@
$('.external-link').appendChild(externalLink);
}
$('.header').classList.add('meta-init');
$('.header').classList.remove('meta-init-error');
setTimeout(() => $('.lds-spinner') && $('.lds-spinner').remove(), 1000);
showError('');
requestAnimationFrame(adjustCodeHeight);
function makeAuthor(text) {
const match = text.match(/^(.+?)(?:\s+<(.+?)>)?(?:\s+\((.+?)\))$/);
if (!match) {
@ -115,7 +138,7 @@
$element({
tag: 'svg#svg',
viewBox: '0 0 20 20',
class: 'icon',
class: 'svg-icon',
appendChild: $element({
tag: 'svg#path',
d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z'
@ -144,6 +167,12 @@
}
}
function installButtonClass() {
return installed ? 'installed' :
!dup ? 'install' :
versionTest > 0 ? 'update' : 'reinstall';
}
function installButtonLabel() {
return t(
installed ? 'installButtonInstalled' :
@ -154,55 +183,50 @@
}
function showError(err) {
$$('.main .warning').forEach(e => e.remove());
const main = $('.main');
main.insertBefore(buildWarning(err), main.firstChild);
$('.warnings').textContent = '';
if (err) {
$('.warnings').appendChild(buildWarning(err));
}
$('.warnings').classList.toggle('visible', Boolean(err));
$('.container').classList.toggle('has-warnings', Boolean(err));
adjustCodeHeight();
}
function install(style) {
const request = Object.assign(style, {
method: 'saveUsercss',
reason: 'update'
});
return runtimeSend(request)
.then(result => {
installed = result;
installed = style;
$$('.warning')
.forEach(el => el.remove());
$('.install').disabled = true;
$('.install').classList.add('installed');
$('.set-update-url input[type=checkbox]').disabled = true;
$('.set-update-url').title = result.updateUrl ?
t('installUpdateFrom', result.updateUrl) : '';
$$('.warning')
.forEach(el => el.remove());
$('button.install').disabled = true;
$('button.install').classList.add('installed');
$('h2.installed').classList.add('active');
$('.set-update-url input[type=checkbox]').disabled = true;
$('.set-update-url').title = style.updateUrl ?
t('installUpdateFrom', style.updateUrl) : '';
updateMeta(result);
updateMeta(style);
chrome.runtime.sendMessage({method: 'openEditor', id: result.id});
if (!liveReload && !prefs.get('openEditInWindow')) {
chrome.tabs.update({url: '/edit.html?id=' + style.id});
} else {
BG.openEditor(style.id);
if (!liveReload) {
closeCurrentTab();
}
}
if (!liveReload) {
port.postMessage({method: 'closeTab'});
}
window.dispatchEvent(new CustomEvent('installed'));
})
.catch(err => {
messageBox.alert(chrome.i18n.getMessage('styleInstallFailed', String(err)));
});
window.dispatchEvent(new CustomEvent('installed'));
}
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
runtimeSend({
method: 'buildUsercss',
sourceCode,
checkDup: true
}).then(init, initError);
}
function initError(err) {
$('.main').insertBefore(buildWarning(err), $('.main').childNodes[0]);
$('.header').style.display = 'none';
cm.refresh();
sendMessage({method: 'buildUsercss', sourceCode, checkDup: true})
.then(init)
.catch(err => {
$('.header').classList.add('meta-init-error');
showError(err);
});
}
function buildWarning(err) {
@ -227,17 +251,17 @@
);
}
$('button.install').onclick = () => {
const message = dup ?
chrome.i18n.getMessage('styleInstallOverwrite', [
data.name, dupData.version, data.version
]) :
chrome.i18n.getMessage('styleInstall', [data.name]);
messageBox.confirm(message).then(result => {
if (result) {
return install(style);
}
});
(!dup ?
Promise.resolve(true) :
messageBox.confirm(t('styleInstallOverwrite', [
data.name,
dupData.version,
data.version,
]))
).then(ok => ok &&
sendMessage(Object.assign(style, {method: 'saveUsercss', reason: 'update'}))
.then(install)
.catch(err => messageBox.alert(t('styleInstallFailed', err))));
};
// set updateUrl
@ -260,6 +284,10 @@
}
};
if (!port) {
return;
}
// live reload
const setLiveReload = $('.live-reload input[type=checkbox]');
if (updateUrl.protocol !== 'file:') {
@ -296,4 +324,49 @@
}
return result;
}
function adjustCodeHeight() {
// Chrome-only bug (apparently): it doesn't limit the scroller element height
const scroller = cm.display.scroller;
const prevWindowHeight = adjustCodeHeight.prevWindowHeight;
if (scroller.scrollHeight === scroller.clientHeight ||
prevWindowHeight && window.innerHeight !== prevWindowHeight) {
adjustCodeHeight.prevWindowHeight = window.innerHeight;
cm.setSize(null, $('.main').offsetHeight - $('.warnings').offsetHeight);
}
}
function getCodeDirectly() {
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
// To circumvent that, the bg process downloads the code directly
const key = 'tempUsercssCode' + tabId;
chrome.storage.local.get(key, data => {
const code = data && data[key];
// bg already downloaded the code
if (typeof code === 'string') {
initSourceCode(code);
chrome.storage.local.remove(key);
return;
}
// bg still downloads the code
if (code && code.loading) {
const waitForCodeInStorage = (changes, area) => {
if (area === 'local' && key in changes) {
initSourceCode(changes[key].newValue);
chrome.storage.onChanged.removeListener(waitForCodeInStorage);
chrome.storage.local.remove(key);
}
};
chrome.storage.onChanged.addListener(waitForCodeInStorage);
return;
}
// on the off-chance dbExecChromeStorage.getAll ran right after bg download was saved
download(params.get('updateUrl'))
.then(initSourceCode)
.catch(err => messageBox.alert(t('styleInstallFailed', String(err))));
});
}
})();

View File

@ -47,7 +47,7 @@ onDOMready().then(() => {
}
});
if (!chrome.app) {
if (!chrome.app && chrome.windows) {
// die if unable to access BG directly
chrome.windows.getCurrent(wnd => {
if (!BG && wnd.incognito) {
@ -89,11 +89,14 @@ function onDOMready() {
}
function scrollElementIntoView(element) {
function scrollElementIntoView(element, {invalidMarginRatio = 0} = {}) {
// align to the top/bottom of the visible area if wasn't visible
const bounds = element.getBoundingClientRect();
if (bounds.top < 0 || bounds.top > innerHeight - bounds.height) {
element.scrollIntoView(bounds.top < 0);
const {top, height} = element.getBoundingClientRect();
const {top: parentTop, bottom: parentBottom} = element.parentNode.getBoundingClientRect();
const windowHeight = window.innerHeight;
if (top < Math.max(parentTop, windowHeight * invalidMarginRatio) ||
top > Math.min(parentBottom, windowHeight) - height - windowHeight * invalidMarginRatio) {
window.scrollBy(0, top - windowHeight / 2 + height);
}
}
@ -165,7 +168,7 @@ function $element(opt) {
const element = ns
? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
: document.createElement(tag || 'div');
const children = opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild];
const children = Array.isArray(opt.appendChild) ? opt.appendChild : [opt.appendChild];
for (const child of children) {
if (child) {
element.appendChild(child instanceof Node ? child : document.createTextNode(child));
@ -208,3 +211,35 @@ function makeLink(href = '', content) {
}
return $element(opt);
}
function initCollapsibles({bindClickOn = 'h2'} = {}) {
const prefMap = {};
const elements = $$('details[data-pref]');
for (const el of elements) {
const key = el.dataset.pref;
prefMap[key] = el;
el.open = prefs.get(key);
(bindClickOn && $(bindClickOn, el) || el).addEventListener('click', onClick);
}
prefs.subscribe(Object.keys(prefMap), (key, value) => {
const el = prefMap[key];
if (el.open !== value) {
el.open = value;
}
});
function onClick(event) {
if (event.target.closest('.intercepts-click')) {
event.preventDefault();
} else {
setTimeout(saveState, 0, event.target.closest('details'));
}
}
function saveState(el) {
prefs.set(el.dataset.pref, el.open);
}
}

View File

@ -7,6 +7,7 @@ const KEEP_CHANNEL_OPEN = true;
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
const OPERA = CHROME && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
const ANDROID = !chrome.windows;
let FIREFOX = !CHROME && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
if (!CHROME && !chrome.browserAction.openPopup) {
@ -86,7 +87,6 @@ function notifyAllTabs(msg) {
style: getStyleWithNoCode(msg.style)
});
}
const maybeIgnoreLastError = FIREFOX ? ignoreChromeError : undefined;
const affectsAll = !msg.affects || msg.affects.all;
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
const affectsTabs = affectsAll || affectsOwnOriginOnly;
@ -100,7 +100,8 @@ function notifyAllTabs(msg) {
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
&& (!FIREFOX || tab.width)) {
chrome.tabs.sendMessage(tab.id, msg, maybeIgnoreLastError);
msg.tabId = tab.id;
sendMessage(msg, ignoreChromeError);
}
if (affectsIcon && BG) {
BG.updateIcon(tab);
@ -127,7 +128,34 @@ function notifyAllTabs(msg) {
}
// notify background page and all open popups
if (affectsSelf) {
chrome.runtime.sendMessage(msg, maybeIgnoreLastError);
msg.tabId = undefined;
sendMessage(msg, ignoreChromeError);
}
}
function sendMessage(msg, callback) {
/*
Promise mode [default]:
- rejects on receiving {__ERROR__: message} created by background.js::onRuntimeMessage
- automatically suppresses chrome.runtime.lastError because it's autogenerated
by browserAction.setText which lacks a callback param in chrome API
Standard callback mode:
- enabled by passing a second param
*/
const {tabId, frameId} = msg;
const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage;
const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg];
if (callback) {
fn(...args, callback);
} else {
return new Promise((resolve, reject) => {
fn(...args, r => {
const err = r && r.__ERROR__;
(err ? reject : resolve)(err || r);
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
});
});
}
}
@ -204,7 +232,8 @@ function openURL({url, index, openerTabId, currentWindow = true}) {
} else {
// create a new tab
const options = {url, index};
if (tab && (!FIREFOX || FIREFOX >= 57) && !chromeInIncognito) {
// FF57+ supports openerTabId, but not in Android (indicated by the absence of chrome.windows)
if (tab && (!FIREFOX || FIREFOX >= 57 && chrome.windows) && !chromeInIncognito) {
options.openerTabId = tab.id;
}
chrome.tabs.create(options, resolve);
@ -220,7 +249,7 @@ function activateTab(tab) {
new Promise(resolve => {
chrome.tabs.update(tab.id, {active: true}, resolve);
}),
new Promise(resolve => {
chrome.windows && new Promise(resolve => {
chrome.windows.update(tab.windowId, {focused: true}, resolve);
}),
]);
@ -336,7 +365,7 @@ function sessionStorageHash(name) {
function onBackgroundReady() {
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
sendMessage({method: 'healthCheck'}, health => {
if (health !== undefined) {
BG = chrome.extension.getBackgroundPage();
resolve();
@ -379,49 +408,47 @@ function deleteStyleSafe({id, notify = true} = {}) {
}
function download(url) {
function download(url, {
method = url.includes('?') ? 'POST' : 'GET',
body = url.includes('?') ? url.slice(url.indexOf('?')) : null,
requiredStatusCode = 200,
timeout = 10e3,
headers = {
'Content-type': 'application/x-www-form-urlencoded',
},
} = {}) {
return new Promise((resolve, reject) => {
url = new URL(url);
const TIMEOUT = 10000;
const options = {
method: url.search ? 'POST' : 'GET',
body: url.search ? url.search.slice(1) : null,
headers: {
'Content-type': 'application/x-www-form-urlencoded'
}
};
if (url.protocol === 'file:' && FIREFOX) {
// https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
options.mode = 'same-origin';
// FIXME: add FetchController when it is available.
// https://developer.mozilla.org/en-US/docs/Web/API/FetchController/abort
let timer;
const timer = setTimeout(reject, timeout, new Error('Timeout fetching ' + url.href));
fetch(url.href, {mode: 'same-origin'})
.then(r => {
clearTimeout(timer);
if (r.status !== 200) {
throw r.status;
}
return r.text();
return r.status === 200 ? r.text() : Promise.reject(r.status);
})
.then(resolve, reject);
timer = setTimeout(
() => reject(new Error(`Fetch URL timeout: ${url.href}`)),
TIMEOUT
);
.catch(reject)
.then(resolve);
return;
}
const xhr = new XMLHttpRequest();
xhr.timeout = TIMEOUT;
xhr.onload = () => (xhr.status === 200 || url.protocol === 'file:'
? resolve(xhr.responseText)
: reject(xhr.status));
xhr.onerror = reject;
xhr.open(options.method, url.href, true);
for (const key of Object.keys(options.headers)) {
xhr.setRequestHeader(key, options.headers[key]);
xhr.timeout = timeout;
xhr.onloadend = event => {
if (event.type !== 'error' && (
xhr.status === requiredStatusCode || !requiredStatusCode ||
url.protocol === 'file:')) {
resolve(xhr.responseText);
} else {
reject(xhr.status);
}
};
xhr.onerror = xhr.onloadend;
xhr.open(method, url.href, true);
for (const key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
xhr.send(options.body);
xhr.send(body);
});
}
@ -438,7 +465,7 @@ function openEditor(id) {
if (id) {
url += `?id=${id}`;
}
if (prefs.get('openEditInWindow')) {
if (chrome.windows && prefs.get('openEditInWindow')) {
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
} else {
openURL({url});

View File

@ -18,18 +18,15 @@ var mozParser = (() => {
parser.addListener('startdocument', e => {
const lastSection = sectionStack[sectionStack.length - 1];
let outerText = getRange(lastSection.start, {line: e.line, col: e.col - 1});
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
const section = {
code: '',
start: {
line: parser._tokenStream._token.endLine,
col: parser._tokenStream._token.endCol,
},
};
const lastCmt = getLastComment(outerText);
const {endLine: line, endCol: col} = parser._tokenStream._token;
const section = {code: '', start: {line, col}};
// move last comment before @-moz-document inside the section
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
section.code = gapComment[1] + '\n';
outerText = outerText.substring(0, gapComment.index).trim();
if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) {
section.code = lastCmt + '\n';
const indent = outerText.match(/^\s*/)[0];
outerText = outerText.slice(0, -lastCmt.length);
outerText = indent + outerText.trim();
}
if (outerText.trim()) {
lastSection.code = outerText;
@ -118,13 +115,36 @@ var mozParser = (() => {
const first = s.charAt(0);
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
}
function getLastComment(text) {
let open = text.length;
let close;
while (open) {
// at this point we're guaranteed to be outside of a comment
close = text.lastIndexOf('*/', open - 2);
if (close < 0) {
break;
}
// stop if a non-whitespace precedes and return what we currently have
const tailEmpty = !text.substring(close + 2, open).trim();
if (!tailEmpty) {
break;
}
// find a closed preceding comment
const prevClose = text.lastIndexOf('*/', close);
// then find the real start of current comment
// e.g. /* preceding */ /* current /* current /* current */
open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2);
}
return text.substr(open);
}
});
}
return {
// Parse mozilla-format userstyle into sections
parse(text) {
return loadScript('/vendor-overwrites/csslint/csslint-worker.js')
return Promise.resolve(self.CSSLint || loadScript('/vendor-overwrites/csslint/csslint-worker.js'))
.then(() => parseMozFormat(text));
},
format(style) {

View File

@ -19,15 +19,21 @@ var prefs = new function Prefs() {
'manage.onlyEnabled': false, // display only enabled styles
'manage.onlyLocal': false, // display only styles created locally
'manage.onlyUsercss': false, // display only usercss styles
'manage.onlyEnabled.invert': false, // display only disabled styles
'manage.onlyLocal.invert': false, // display only externally installed styles
'manage.newUI': true, // use the new compact layout
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
// UI element state: expanded/collapsed
'manage.options.expanded': true,
// the new compact layout doesn't look good on Android yet
'manage.newUI': !navigator.appVersion.includes('Android'),
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
'manage.newUI.faviconsGray': true, // gray out favicons
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
'editor.options': {}, // CodeMirror.defaults.*
'editor.options.expanded': true, // UI element state: expanded/collapsed
'editor.lint.expanded': true, // UI element state: expanded/collapsed
'editor.lineWrapping': true, // word wrap
'editor.smartIndent': true, // 'smart' indent
'editor.indentWithTabs': false, // smart indent with tabs
@ -56,6 +62,15 @@ var prefs = new function Prefs() {
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
// show CSS colors as clickable colored rectangles
'editor.colorpicker': true,
// #DEAD or #beef
'editor.colorpicker.hexUppercase': false,
// default hotkey
'editor.colorpicker.hotkey': '',
// last color
'editor.colorpicker.color': '',
'iconset': 0, // 0 = dark-themed icon
// 1 = light-themed icon
@ -136,9 +151,13 @@ var prefs = new function Prefs() {
}
}
if (hasChanged) {
const listener = onChange.specific.get(key);
if (listener) {
listener(key, value);
const specific = onChange.specific.get(key);
if (typeof specific === 'function') {
specific(key, value);
} else if (specific instanceof Set) {
for (const listener of specific.values()) {
listener(key, value);
}
}
for (const listener of onChange.any.values()) {
listener(key, value);
@ -164,7 +183,14 @@ var prefs = new function Prefs() {
// listener: function (key, value)
if (keys) {
for (const key of keys) {
onChange.specific.set(key, listener);
const existing = onChange.specific.get(key);
if (!existing) {
onChange.specific.set(key, listener);
} else if (existing instanceof Set) {
existing.add(listener);
} else {
onChange.specific.set(key, new Set([existing, listener]));
}
}
} else {
onChange.any.add(listener);
@ -275,7 +301,7 @@ var prefs = new function Prefs() {
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
function getSync() {
if ('sync' in chrome.storage) {
if ('sync' in chrome.storage && !chrome.runtime.id.includes('@temporary')) {
return chrome.storage.sync;
}
const crappyStorage = {};
@ -289,7 +315,9 @@ var prefs = new function Prefs() {
crappyStorage[property] = source[property];
}
}
callback();
if (typeof callback === 'function') {
callback();
}
}
};
}

View File

@ -1,4 +1,4 @@
/* global loadScript mozParser semverCompare colorParser */
/* global loadScript mozParser semverCompare colorParser styleCodeEmpty */
'use strict';
// eslint-disable-next-line no-var
@ -30,9 +30,10 @@ var usercss = (() => {
':root {\n' +
Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('') +
'}\n';
for (const section of sections) {
section.code = varDef + section.code;
if (!styleCodeEmpty(section.code)) {
section.code = varDef + section.code;
}
}
}
},
@ -95,6 +96,14 @@ var usercss = (() => {
}
};
const RX_NUMBER = /-?\d+(\.\d+)?\s*/y;
const RX_WHITESPACE = /\s*/y;
const RX_WORD = /([\w-]+)\s*/y;
const RX_STRING_BACKTICK = /(`(?:\\`|[\s\S])*?`)\s*/y;
const RX_STRING_QUOTED = /((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/y;
const worker = {};
function getMetaSource(source) {
const commentRe = /\/\*[\s\S]*?\*\//g;
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
@ -111,12 +120,14 @@ var usercss = (() => {
};
}
}
return {text: '', index: 0};
}
function parseWord(state, error = 'invalid word') {
const match = state.text.slice(state.re.lastIndex).match(/^([\w-]+)\s*/);
RX_WORD.lastIndex = state.re.lastIndex;
const match = RX_WORD.exec(state.text);
if (!match) {
throw new Error(error);
throw new Error((state.errorPrefix || '') + error);
}
state.value = match[1];
state.re.lastIndex += match[0].length;
@ -134,6 +145,7 @@ var usercss = (() => {
parseWord(state, 'missing type');
result.type = state.type = state.value;
if (!META_VARS.includes(state.type)) {
throw new Error(`unknown type: ${state.type}`);
}
@ -144,52 +156,69 @@ var usercss = (() => {
parseString(state);
result.label = state.value;
if (state.type === 'checkbox') {
const match = state.text.slice(state.re.lastIndex).match(/([01])\s+/);
if (!match) {
throw new Error('value must be 0 or 1');
}
state.re.lastIndex += match[0].length;
result.default = match[1];
} else if (state.type === 'select' || (state.type === 'image' && state.key === 'var')) {
parseJSONValue(state);
if (Array.isArray(state.value)) {
result.options = state.value.map(text => createOption(text));
} else {
result.options = Object.keys(state.value).map(k => createOption(k, state.value[k]));
}
result.default = result.options[0].name;
} else if (state.type === 'dropdown' || state.type === 'image') {
if (state.text[state.re.lastIndex] !== '{') {
throw new Error('no open {');
}
result.options = [];
state.re.lastIndex++;
while (state.text[state.re.lastIndex] !== '}') {
const option = {};
const {re, type, text} = state;
parseStringUnquoted(state);
option.name = state.value;
parseString(state);
option.label = state.value;
if (state.type === 'dropdown') {
parseEOT(state);
} else {
parseString(state);
switch (type === 'image' && state.key === 'var' ? '@image@var' : type) {
case 'checkbox': {
const match = text.slice(re.lastIndex).match(/([01])\s+/);
if (!match) {
throw new Error('value must be 0 or 1');
}
option.value = state.value;
result.options.push(option);
re.lastIndex += match[0].length;
result.default = match[1];
break;
}
case 'select':
case '@image@var': {
state.errorPrefix = 'Invalid JSON: ';
parseJSONValue(state);
state.errorPrefix = '';
if (Array.isArray(state.value)) {
result.options = state.value.map(text => createOption(text));
} else {
result.options = Object.keys(state.value).map(k => createOption(k, state.value[k]));
}
result.default = (result.options[0] || {}).name || '';
break;
}
case 'dropdown':
case 'image': {
if (text[re.lastIndex] !== '{') {
throw new Error('no open {');
}
result.options = [];
re.lastIndex++;
while (text[re.lastIndex] !== '}') {
const option = {};
parseStringUnquoted(state);
option.name = state.value;
parseString(state);
option.label = state.value;
if (type === 'dropdown') {
parseEOT(state);
} else {
parseString(state);
}
option.value = state.value;
result.options.push(option);
}
re.lastIndex++;
eatWhitespace(state);
result.default = result.options[0].name;
break;
}
default: {
// text, color
parseStringToEnd(state);
result.default = state.value;
}
state.re.lastIndex++;
eatWhitespace(state);
result.default = result.options[0].name;
} else {
// text, color
parseStringToEnd(state);
result.default = state.value;
}
state.usercssData.vars[result.name] = result;
validVar(result);
@ -223,19 +252,20 @@ var usercss = (() => {
}
function parseStringUnquoted(state) {
const re = /[^"]*/y;
re.lastIndex = state.re.lastIndex;
const match = state.text.match(re);
state.re.lastIndex += match[0].length;
state.value = match[0].trim().replace(/\s+/g, '-');
const pos = state.re.lastIndex;
const nextQuoteOrEOL = posOrEnd(state.text, '"', pos);
state.re.lastIndex = nextQuoteOrEOL;
state.value = state.text.slice(pos, nextQuoteOrEOL).trim().replace(/\s+/g, '-');
}
function parseString(state) {
const match = state.text.slice(state.re.lastIndex).match(
state.text[state.re.lastIndex] === '`' ?
/^(`(?:\\`|[\s\S])*?`)\s*/ :
/^((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/
);
const pos = state.re.lastIndex;
const rx = state.text[pos] === '`' ? RX_STRING_BACKTICK : RX_STRING_QUOTED;
rx.lastIndex = pos;
const match = rx.exec(state.text);
if (!match) {
throw new Error((state.errorPrefix || '') + 'Quoted string expected');
}
state.re.lastIndex += match[0].length;
state.value = unquote(match[1]);
}
@ -247,87 +277,91 @@ var usercss = (() => {
'true': true,
'false': false
};
if (state.text[state.re.lastIndex] === '{') {
const {text, re, errorPrefix} = state;
if (text[re.lastIndex] === '{') {
// object
const obj = {};
state.re.lastIndex++;
re.lastIndex++;
eatWhitespace(state);
while (state.text[state.re.lastIndex] !== '}') {
while (text[re.lastIndex] !== '}') {
parseString(state);
const key = state.value;
if (state.text[state.re.lastIndex] !== ':') {
throw new Error('missing \':\'');
if (text[re.lastIndex] !== ':') {
throw new Error(`${errorPrefix}missing ':'`);
}
state.re.lastIndex++;
re.lastIndex++;
eatWhitespace(state);
parseJSONValue(state);
obj[key] = state.value;
if (state.text[state.re.lastIndex] === ',') {
state.re.lastIndex++;
if (text[re.lastIndex] === ',') {
re.lastIndex++;
eatWhitespace(state);
} else if (state.text[state.re.lastIndex] !== '}') {
throw new Error('missing \',\' or \'}\'');
} else if (text[re.lastIndex] !== '}') {
throw new Error(`${errorPrefix}missing ',' or '}'`);
}
}
state.re.lastIndex++;
re.lastIndex++;
eatWhitespace(state);
state.value = obj;
} else if (state.text[state.re.lastIndex] === '[') {
} else if (text[re.lastIndex] === '[') {
// array
const arr = [];
state.re.lastIndex++;
re.lastIndex++;
eatWhitespace(state);
while (state.text[state.re.lastIndex] !== ']') {
while (text[re.lastIndex] !== ']') {
parseJSONValue(state);
arr.push(state.value);
if (state.text[state.re.lastIndex] === ',') {
state.re.lastIndex++;
if (text[re.lastIndex] === ',') {
re.lastIndex++;
eatWhitespace(state);
} else if (state.text[state.re.lastIndex] !== ']') {
throw new Error('missing \',\' or \']\'');
} else if (text[re.lastIndex] !== ']') {
throw new Error(`${errorPrefix}missing ',' or ']'`);
}
}
state.re.lastIndex++;
re.lastIndex++;
eatWhitespace(state);
state.value = arr;
} else if (state.text[state.re.lastIndex] === '"' || state.text[state.re.lastIndex] === '`') {
} else if (text[re.lastIndex] === '"' || text[re.lastIndex] === '`') {
// string
parseString(state);
} else if (/\d/.test(state.text[state.re.lastIndex])) {
} else if (/\d/.test(text[re.lastIndex])) {
// number
parseNumber(state);
} else {
parseWord(state);
if (!(state.value in JSON_PRIME)) {
throw new Error(`unknown literal '${state.value}'`);
throw new Error(`${errorPrefix}unknown literal '${state.value}'`);
}
state.value = JSON_PRIME[state.value];
}
}
function parseNumber(state) {
const match = state.slice(state.re.lastIndex).match(/^-?\d+(\.\d+)?\s*/);
RX_NUMBER.lastIndex = state.re.lastIndex;
const match = RX_NUMBER.exec(state.text);
if (!match) {
throw new Error('invalid number');
throw new Error((state.errorPrefix || '') + 'invalid number');
}
state.value = Number(match[0].trim());
state.re.lastIndex += match[0].length;
}
function eatWhitespace(state) {
const match = state.text.slice(state.re.lastIndex).match(/\s*/);
state.re.lastIndex += match[0].length;
RX_WHITESPACE.lastIndex = state.re.lastIndex;
state.re.lastIndex += RX_WHITESPACE.exec(state.text)[0].length;
}
function parseStringToEnd(state) {
const match = state.text.slice(state.re.lastIndex).match(/.+/);
state.value = unquote(match[0].trim());
state.re.lastIndex += match[0].length;
rewindToEOL(state);
const EOL = posOrEnd(state.text, '\n', state.re.lastIndex);
const match = state.text.slice(state.re.lastIndex, EOL);
state.value = unquote(match.trim());
state.re.lastIndex += match.length;
}
function unquote(s) {
const q = s[0];
if (q === s[s.length - 1] && /['"`]/.test(q)) {
if (q === s[s.length - 1] && (q === '"' || q === "'" || q === '`')) {
// http://www.json.org/
return s.slice(1, -1).replace(
new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'),
@ -342,6 +376,15 @@ var usercss = (() => {
return s;
}
function posOrEnd(haystack, needle, start) {
const pos = haystack.indexOf(needle, start);
return pos < 0 ? haystack.length : pos;
}
function rewindToEOL({re, text}) {
re.lastIndex -= text[re.lastIndex - 1] === '\n' ? 1 : 0;
}
function buildMeta(sourceCode) {
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
@ -393,7 +436,9 @@ var usercss = (() => {
doParse();
} catch (e) {
// grab additional info
e.index = metaIndex + state.re.lastIndex;
let pos = state.re.lastIndex;
while (pos && /[\s\n]/.test(state.text[--pos])) { /**/ }
e.index = metaIndex + pos;
throw e;
}
@ -431,25 +476,11 @@ var usercss = (() => {
const sVars = simpleVars(vars);
return Promise.resolve().then(() => {
// preprocess
if (builder.preprocess) {
return builder.preprocess(sourceCode, sVars);
}
return sourceCode;
}).then(mozStyle =>
// moz-parser
loadScript('/js/moz-parser.js').then(() =>
mozParser.parse(mozStyle).then(sections => {
style.sections = sections;
})
)
).then(() => {
// postprocess
if (builder.postprocess) {
return builder.postprocess(style.sections, sVars);
}
}).then(() => style);
return Promise.resolve(builder.preprocess && builder.preprocess(sourceCode, sVars) || sourceCode)
.then(mozStyle => invokeWorker({action: 'parse', code: mozStyle}))
.then(sections => (style.sections = sections))
.then(() => builder.postprocess && builder.postprocess(style.sections, sVars))
.then(() => style);
}
function simpleVars(vars) {
@ -536,5 +567,24 @@ var usercss = (() => {
}
}
function invokeWorker(message) {
if (!worker.queue) {
worker.instance = new Worker('/vendor-overwrites/csslint/csslint-worker.js');
worker.queue = [];
worker.instance.onmessage = ({data}) => {
worker.queue.shift().resolve(data);
if (worker.queue.length) {
worker.instance.postMessage(worker.queue[0].message);
}
};
}
return new Promise(resolve => {
worker.queue.push({message, resolve});
if (worker.queue.length === 1) {
worker.instance.postMessage(message);
}
});
}
return {buildMeta, buildCode, assignVars};
})();

View File

@ -1,11 +1,13 @@
<html id="stylus">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title i18n-text="manageTitle"></title>
<link rel="stylesheet" href="manage/manage.css">
<link rel="stylesheet" href="msgbox/msgbox.css">
<link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
<style id="style-overrides"></style>
@ -86,10 +88,8 @@
<template data-id="configureIcon">
<span class="configure-usercss" i18n-title="configureStyle">
<svg class="svg-icon" viewBox="0 0 20 20">
<path
d="M 10,2.0423828 A 7.9575898,7.9575898 0 0 0 8.8908203,2.1285156 V 4.355664 A 5.7578608,5.7578608 0 0 0 6.7919922,5.2240235 l -1.575,-1.575 A 7.9575898,7.9575898 0 0 0 3.6507813,5.21875 L 5.2222656,6.7902344 A 5.7578608,5.7578608 0 0 0 4.3521485,8.8908203 H 2.1302735 A 7.9575898,7.9575898 0 0 0 2.0423828,10 7.9575898,7.9575898 0 0 0 2.1285156,11.10918 H 4.355664 a 5.7578608,5.7578608 0 0 0 0.8683595,2.098828 l -1.575,1.575 A 7.9575898,7.9575898 0 0 0 5.21875,16.349219 l 1.5714844,-1.571484 a 5.7578608,5.7578608 0 0 0 2.1005859,0.870117 v 2.221875 A 7.9575898,7.9575898 0 0 0 10,17.957617 a 7.9575898,7.9575898 0 0 0 1.10918,-0.08613 v -2.227149 a 5.7578608,5.7578608 0 0 0 2.098828,-0.868359 l 1.575,1.575 a 7.9575898,7.9575898 0 0 0 1.566211,-1.569727 l -1.571484,-1.571485 a 5.7578608,5.7578608 0 0 0 0.870117,-2.100585 h 2.221875 A 7.9575898,7.9575898 0 0 0 17.957617,10 7.9575898,7.9575898 0 0 0 17.871485,8.8908203 H 15.644336 A 5.7578608,5.7578608 0 0 0 14.775977,6.7919922 l 1.575,-1.575 A 7.9575898,7.9575898 0 0 0 14.78125,3.6507813 L 13.209765,5.2222656 A 5.7578608,5.7578608 0 0 0 11.10918,4.3521485 V 2.1302735 A 7.9575898,7.9575898 0 0 0 10,2.0423828 Z m 0,4.2574219 A 3.6994645,3.6994645 0 0 1 13.700195,10 3.6994645,3.6994645 0 0 1 10,13.700195 3.6994645,3.6994645 0 0 1 6.2998047,10 3.6994645,3.6994645 0 0 1 10,6.2998047 Z"
/>
<svg class="svg-icon configure" viewBox="0 0 16 16">
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"></path>
</svg>
</span>
</template>
@ -138,7 +138,7 @@
<template data-id="extraAppliesTo">
<details class="applies-to-extra">
<summary i18n-text="appliesDisplayTruncatedSuffix"></summary>
<summary class="applies-to-extra-expander" i18n-text="appliesDisplayTruncatedSuffix"></summary>
</details>
</template>
@ -150,7 +150,7 @@
<script src="manage/filters.js"></script>
<script src="manage/updater-ui.js"></script>
<script src="manage/object-diff.js"></script>
<script src="js/color-parser.js"></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="manage/config-dialog.js"></script>
<script src="manage/manage.js"></script>
</head>
@ -173,13 +173,22 @@
</label>
<label>
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable)"
data-filter-hide=".updatable">
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done">
<select id="manage.onlyLocal.invert" i18n-title="manageOnlyLocalTooltip">
<option i18n-text="manageOnlyLocal" value="false"></option>
<option i18n-text="manageOnlyExternal" value="true"></option>
</select>
</label>
<label>
<input id="manage.onlyUsercss" type="checkbox"
data-filter=".usercss"
data-filter-hide=":not(.usercss)">
<select id="manage.onlyUsercss.invert">
<option i18n-text="manageOnlyUsercss" value="false"></option>
<option i18n-text="manageOnlyNonUsercss" value="true"></option>
</select>
</label>
<label id="onlyUpdates" class="hidden">
<input type="checkbox"
data-filter=".can-update, .update-problem, .update-done"
@ -208,9 +217,20 @@
<a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel"></button>
</a>
<label id="newStyleAsUsercss-wrapper" class="nobreak">
<input type="checkbox" id="newStyleAsUsercss">
<span i18n-text="manageNewStyleAsUsercss" i18n-title="optionsAdvancedNewStyleAsUsercss"></span>
<a id="usercss-wiki"
href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n-title="externalUsercssDocument">
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
</svg>
</a>
</label>
</p>
<div id="options">
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
<details id="options" data-pref="manage.options.expanded">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<label><input id="manage.newUI" type="checkbox"><span i18n-text="manageNewUI"></span></label>
<div id="newUIoptions">
<div>
@ -238,7 +258,7 @@
i18n-title="editorStylesButton"
target="_blank"><button i18n-text="cm_theme"></button></a>
</p>
</div>
</details>
<div id="backup">
<h2 id="backup-title" i18n-text="backupButtons"></h2>
<span id="backup-message" i18n-text="backupMessage"></span>
@ -251,8 +271,9 @@
</div>
<div id="installed"></div>
<script src="manage/fileSaveLoad.js"></script>
<script src="manage/import-export.js"></script>
<script src="msgbox/msgbox.js"></script>
<script src="manage/incremental-search.js" async></script>
</body>
</html>

View File

@ -1,8 +1,15 @@
/* global colorParser messageBox makeLink */
/* global messageBox makeLink */
'use strict';
function configDialog(style) {
const form = buildConfigForm();
const varsHash = deepCopy(style.usercssData.vars) || {};
const varNames = Object.keys(varsHash);
const vars = varNames.map(name => varsHash[name]);
const elements = [];
const colorpicker = window.colorpicker();
buildConfigForm();
renderValues();
return messageBox({
title: `${style.name} v${style.usercssData.version}`,
@ -18,55 +25,87 @@ function configDialog(style) {
}),
$element({
className: 'config-body',
appendChild: form.elements
appendChild: elements
})
],
buttons: [
t('confirmSave'),
{
textContent: t('confirmDefault'),
onclick: form.useDefault
onclick: useDefault
},
t('confirmCancel')
]
}).then(result => {
if (result.button !== 0 && !result.enter) {
}).then(({button, esc}) => {
if (button !== 1) {
colorpicker.hide();
}
if (button > 0 || esc || !vars.length || !vars.some(va => va.dirty)) {
return;
}
return form.getVars();
style.reason = 'config';
const styleVars = style.usercssData.vars;
const bgStyle = BG.cachedStyles.byId.get(style.id);
const bgVars = bgStyle && (bgStyle.usercssData || {}).vars || {};
const invalid = [];
let numValid = 0;
for (const va of vars) {
const bgva = bgVars[va.name];
let error;
if (!bgva) {
error = 'deleted';
delete styleVars[va.name];
} else
if (bgva.type !== va.type) {
error = ['type ', '*' + va.type, ' != ', '*' + bgva.type];
} else
if ((va.type === 'select' || va.type === 'dropdown') &&
va.value !== null && va.value !== undefined &&
bgva.options.every(o => o.name !== va.value)) {
error = `'${va.value}' not in the updated '${va.type}' list`;
} else if (!va.dirty) {
continue;
} else {
styleVars[va.name].value = va.value;
numValid++;
continue;
}
invalid.push(['*' + va.name, ': ', ...error].map(e =>
e[0] === '*' && $element({tag: 'b', textContent: e.slice(1)}) || e));
if (bgva) {
styleVars[va.name].value = deepCopy(bgva);
}
}
if (invalid.length) {
messageBox.alert([
$element({textContent: t('usercssConfigIncomplete'), style: 'max-width: 34em'}),
$element({
tag: 'ol',
style: 'text-align: left',
appendChild: invalid.map(msg => $element({tag: 'li', appendChild: msg})),
}),
]);
}
return numValid && BG.usercssHelper.save(style);
});
function buildConfigForm() {
const labels = [];
const vars = deepCopy(style.usercssData.vars);
for (const key of Object.keys(vars)) {
const va = vars[key];
for (const va of vars) {
let appendChild;
switch (va.type) {
case 'color':
va.inputColor = $element({tag: 'input', type: 'color'});
va.inputAlpha = $element({
tag: 'input',
type: 'range',
min: 0,
max: 1,
title: chrome.i18n.getMessage('alphaChannel'),
step: 'any'
});
va.inputColor.onchange = va.inputAlpha.oninput = () => {
va.dirty = true;
const color = colorParser.parse(va.inputColor.value);
color.a = Number(va.inputAlpha.value);
va.value = colorParser.format(color);
va.inputColor.style.opacity = color.a;
};
appendChild = [
$element({appendChild: [va.inputColor, va.inputAlpha]})
];
appendChild = [$element({
className: 'cm-colorview',
appendChild: va.inputColor = $element({
va,
className: 'color-swatch',
onclick: showColorpicker,
})
})];
break;
case 'checkbox':
va.input = $element({tag: 'input', type: 'checkbox'});
va.input = $element({tag: 'input', type: 'checkbox', className: 'slider'});
va.input.onchange = () => {
va.dirty = true;
va.value = String(Number(va.input.checked));
@ -105,52 +144,69 @@ function configDialog(style) {
appendChild = [va.input];
break;
}
appendChild.unshift($element({tag: 'span', appendChild: va.label}));
labels.push($element({
elements.push($element({
tag: 'label',
className: `config-${va.type}`,
appendChild
appendChild: [
$element({tag: 'span', appendChild: va.label}),
...appendChild,
],
}));
}
drawValues();
}
function drawValues() {
for (const key of Object.keys(vars)) {
const va = vars[key];
const value = va.value === null || va.value === undefined ?
va.default : va.value;
if (va.type === 'color') {
const color = colorParser.parse(value);
va.inputAlpha.value = color.a;
va.inputColor.style.opacity = color.a;
delete color.a;
va.inputColor.value = colorParser.formatHex(color);
} else if (va.type === 'checkbox') {
va.input.checked = Number(value);
} else {
va.input.value = value;
function renderValues() {
for (const va of vars) {
const useDefault = va.value === null || va.value === undefined;
const value = useDefault ? va.default : va.value;
if (va.type === 'color') {
va.inputColor.style.backgroundColor = value;
if (colorpicker.options.va === va) {
colorpicker.setColor(value);
}
} else if (va.type === 'checkbox') {
va.input.checked = Number(value);
} else {
va.input.value = value;
}
}
}
function useDefault() {
for (const key of Object.keys(vars)) {
const va = vars[key];
va.dirty = va.value !== null && va.value !== undefined && va.value !== va.default;
va.value = null;
}
drawValues();
function useDefault() {
for (const va of vars) {
const hasValue = va.value !== null && va.value !== undefined;
va.dirty = hasValue && va.value !== va.default;
va.value = null;
}
renderValues();
}
function getVars() {
return vars;
function showColorpicker() {
window.removeEventListener('keydown', messageBox.listeners.key, true);
const box = $('#message-box-contents');
colorpicker.show({
va: this.va,
color: this.va.value || this.va.default,
top: this.getBoundingClientRect().bottom - 5,
left: box.getBoundingClientRect().left - 360,
hideDelay: 1e6,
guessBrightness: box,
callback: onColorChanged,
});
}
function onColorChanged(newColor) {
if (newColor) {
this.va.dirty = true;
this.va.value = newColor;
this.va.inputColor.style.backgroundColor = newColor;
}
debounce(restoreEscInDialog);
}
return {
elements: labels,
useDefault,
getVars
};
function restoreEscInDialog() {
if (!$('.colorpicker-popup') && messageBox.element) {
window.addEventListener('keydown', messageBox.listeners.key, true);
}
}
}

View File

@ -8,7 +8,8 @@ const filtersSelector = {
numTotal: 0,
};
const urlFilterParam = new URLSearchParams(location.search).get('url');
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const urlFilterParam = new URLSearchParams(location.search.replace(/^\?/, '')).get('url');
if (location.search) {
history.replaceState(0, document.title, location.origin + location.pathname);
}

View File

@ -288,8 +288,8 @@ function importFromString(jsonString) {
if (tab.id === ownTab.id) {
applyOnMessage(message);
} else {
invokeOrPostpone(tab.id === activeTab.id,
chrome.tabs.sendMessage, tab.id, message, ignoreChromeError);
message.tabId = tab.id;
invokeOrPostpone(tab.id === activeTab.id, sendMessage, message, ignoreChromeError);
}
setTimeout(BG.updateIcon, 0, tab, styles);
if (tab === lastTab) {

View File

@ -0,0 +1,132 @@
/* global installed */
'use strict';
onDOMready().then(() => {
let prevText, focusedLink, focusedEntry;
let prevTime = performance.now();
let focusedName = '';
const input = $element({
tag: 'textarea',
spellcheck: false,
oninput: incrementalSearch,
});
replaceInlineStyle({
position: 'absolute',
color: 'transparent',
border: '1px solid hsla(180, 100%, 100%, .5)',
top: '-1000px',
overflow: 'hidden',
resize: 'none',
'background-color': 'hsla(180, 100%, 100%, .2)',
'pointer-events': 'none',
});
document.body.appendChild(input);
window.addEventListener('keydown', maybeRefocus, true);
function incrementalSearch({which}, immediately) {
if (!immediately) {
debounce(incrementalSearch, 100, {}, true);
return;
}
const direction = which === 38 ? -1 : which === 40 ? 1 : 0;
const text = input.value.toLocaleLowerCase();
if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) {
prevText = text;
return;
}
let textAtPos = 1e6;
let rotated;
const entries = [...installed.children];
const focusedIndex = entries.indexOf(focusedEntry);
if (focusedIndex > 0) {
if (direction > 0) {
rotated = entries.slice(focusedIndex + 1).concat(entries.slice(0, focusedIndex + 1));
} else if (direction < 0) {
rotated = entries.slice(0, focusedIndex).reverse().concat(entries.slice(focusedIndex).reverse());
}
}
let found;
for (const entry of rotated || entries) {
const name = entry.styleNameLowerCase;
const pos = name.indexOf(text);
if (pos === 0) {
found = entry;
break;
} else if (pos > 0 && (pos < textAtPos || direction)) {
found = entry;
textAtPos = pos;
if (direction) {
break;
}
}
}
if (found && found !== focusedEntry) {
focusedEntry = found;
focusedLink = $('.style-name-link', found);
focusedName = found.styleNameLowerCase;
scrollElementIntoView(found, {invalidMarginRatio: .25});
animateElement(found, {className: 'highlight-quick'});
resizeTo(focusedLink);
return true;
}
}
function maybeRefocus(event) {
if (event.altKey || event.ctrlKey || event.metaKey ||
event.target.matches('[type="text"], [type="search"]')) {
return;
}
const k = event.which;
// focus search field on "/" key
if (k === 191 && !event.shiftKey) {
event.preventDefault();
$('#search').focus();
return;
}
const time = performance.now();
if (
// 0-9
k >= 48 && k <= 57 ||
// a-z
k >= 65 && k <= 90 ||
// numpad keys
k >= 96 && k <= 111 ||
// marks
k >= 186
) {
input.focus();
if (time - prevTime > 1000) {
input.value = '';
}
prevTime = time;
} else
if (k === 13 && focusedLink) {
focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
} else
if ((k === 38 || k === 40) && !event.shiftKey &&
time - prevTime < 5000 && incrementalSearch(event, true)) {
prevTime = time;
} else
if (event.target === input) {
(focusedLink || document.body).focus();
input.value = '';
}
}
function resizeTo(el) {
const bounds = el.getBoundingClientRect();
const base = document.scrollingElement;
replaceInlineStyle({
left: bounds.left - 2 + base.scrollLeft + 'px',
top: bounds.top - 1 + base.scrollTop + 'px',
width: bounds.width + 4 + 'px',
height: bounds.height + 2 + 'px',
});
}
function replaceInlineStyle(css) {
for (const prop in css) {
input.style.setProperty(prop, css[prop], 'important');
}
}
});

View File

@ -1,3 +1,12 @@
:root {
--header-width: 280px;
--checkbox-width: 24px;
--name-padding-left: 40px;
--name-padding-right: 40px;
--actions-width: 75px;
--onoffswitch-width: 60px;
}
body {
margin: 0;
font: 12px arial, sans-serif;
@ -6,7 +15,7 @@ body {
height: 100%;
}
a {
a, .disabled a:hover {
color: #000;
transition: color .5s;
text-decoration-skip: ink;
@ -31,13 +40,14 @@ select {
}
#header {
width: 280px;
width: var(--header-width);
height: 100vh;
position: fixed;
top: 0;
padding: 15px;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
box-shadow: 0 0 50px -18px black;
overflow: auto;
box-sizing: border-box;
z-index: 9;
@ -55,13 +65,31 @@ select {
text-decoration: none;
}
#add-style-label {
margin-right: .25em;
margin-bottom: .25em;
}
#newStyleAsUsercss-wrapper:not(:hover) input:not(:checked) ~ a svg {
fill: #aaa;
}
.nobreak {
white-space: nowrap;
}
label.nobreak input {
vertical-align: middle;
margin: 0;
}
.firefox .chromium-only {
display: none;
}
#installed {
position: relative;
padding-left: 280px;
padding-left: var(--header-width);
box-sizing: border-box;
width: 100%;
}
@ -82,22 +110,24 @@ select {
transition: fill .5s;
width: 20px;
height: 20px;
fill: #666;
}
.svg-icon:hover {
fill: #000;
}
.svg-icon {
fill: #666;
}
.svg-icon.info {
width: 14px;
height: 16px;
margin-left: .5ex;
}
.svg-icon.configure {
width: 16px;
height: 16px;
}
.homepage {
margin-left: 0.1em;
margin-right: 0.1em;
@ -121,11 +151,6 @@ select {
text-decoration: none;
}
.style-name-link:hover {
text-decoration: underline;
color: #000;
}
.applies-to {
overflow-wrap: break-word;
}
@ -163,21 +188,21 @@ select {
margin-left: 1ex;
}
summary {
.applies-to-extra-expander {
font-weight: bold;
cursor: pointer;
outline: none;
}
.applies-to-extra summary {
.applies-to-extra-expander {
list-style-type: none; /* for FF, allegedly */
}
.applies-to-extra summary::-webkit-details-marker {
.applies-to-extra-expander::-webkit-details-marker {
display: none;
}
.disabled h2::after {
.disabled h2::after, .entry.usercss .style-name-link::after {
font-weight: normal;
font-size: 11px;
text-transform: lowercase;
@ -187,8 +212,23 @@ summary {
margin-left: 1ex;
}
.disabled {
opacity: 0.5;
.entry.usercss .style-name-link::after {
content: "usercss";
background-color: hsla(180, 100%, 20%, 1);
color: white;
}
.disabled h2 .style-name-link,
.disabled h2::after,
.disabled .actions,
.disabled .applies-to {
opacity: 0.6;
font-weight: normal;
transition: opacity .5s .1s;
}
.disabled:hover .actions {
opacity: 1;
}
.disabled .disable {
@ -199,6 +239,30 @@ summary {
display: none;
}
/* collapsibles */
#header details:not([open]) h2 {
margin-bottom: -.25em;
}
#header summary {
align-items: center;
margin-left: -13px;
cursor: pointer;
outline: none;
}
#header summary h2 {
display: inline-block;
border-bottom: 1px dotted transparent;
margin-top: .2em;
margin-bottom: .4em;
}
#header summary h2:hover {
border-color: #bbb;
}
/* compact layout */
.newUI #installed {
@ -211,11 +275,6 @@ summary {
opacity: 1;
}
.newUI .disabled .style-name,
.newUI .disabled .applies-to {
opacity: .5;
}
.newUI .entry {
display: table-row;
}
@ -234,15 +293,32 @@ summary {
.newUI .checker {
position: relative;
top: 1px;
margin-right: 1ex;
margin: 0 1ex 0 0;
}
.newUI .style-name {
font-size: 14px;
font-family: sans-serif;
text-indent: -2em;
padding-left: 3em;
padding-right: 40px;
text-indent: calc(var(--checkbox-width) - var(--name-padding-left) - 4px);
padding-left: var(--name-padding-left);
padding-right: var(--name-padding-right);
position: relative;
cursor: pointer;
}
.newUI .entry .style-name:hover::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to right, hsla(180, 50%, 30%, 0.2), hsla(180, 20%, 10%, 0.05) 50%, transparent);
pointer-events: none;
}
.newUI .entry.enabled .style-name:hover .style-name-link {
color: hsla(180, 100%, 15%, 1);
}
.newUI .homepage .svg-icon {
@ -252,7 +328,7 @@ summary {
}
.newUI .actions {
width: 75px;
width: var(--actions-width);
height: 20px;
white-space: nowrap;
}
@ -295,6 +371,11 @@ summary {
display: inline;
}
.newUI .up-to-date svg,
.newUI .updated svg {
cursor: auto;
}
.newUI .update-done .updated svg {
top: -4px;
position: relative;
@ -373,7 +454,7 @@ summary {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100vw - 280px - 60px - 25vw - 3rem);
max-width: calc(100vw - var(--header-width) - var(--actions-width) - var(--name-padding-left) - 25vw - var(--name-padding-right));
box-sizing: border-box;
padding-right: 1rem;
line-height: 18px;
@ -532,6 +613,9 @@ input[id^="manage.newUI"] {
.highlight {
animation: highlight 10s cubic-bezier(0,.82,.47,.98);
}
.highlight-quick {
animation: highlight .5s;
}
@keyframes highlight {
from {
@ -559,6 +643,10 @@ fieldset > *:not(legend) {
align-items: center;
}
fieldset > label:hover {
background-color: hsla(0, 0%, 50%, .1);
}
#filters {
border: 1px solid transparent;
}
@ -577,8 +665,7 @@ fieldset > *:not(legend) {
content: ": ";
}
#manage\.onlyEnabled\.invert,
#manage\.onlyLocal\.invert {
fieldset select {
border: none;
max-width: calc(100% - 2em);
background-color: transparent;
@ -694,7 +781,7 @@ fieldset > *:not(legend) {
.config-dialog input,
.config-dialog select,
.config-dialog .onoffswitch {
width: 60px;
width: var(--onoffswitch-width);
margin: 0;
height: 2em;
box-sizing: border-box;
@ -703,7 +790,7 @@ fieldset > *:not(legend) {
.config-dialog select {
width: auto;
min-width: 60px;
min-width: var(--onoffswitch-width);
max-width: 124px;
}
@ -725,6 +812,29 @@ fieldset > *:not(legend) {
margin-right: 4px;
}
.cm-colorview::before,
.color-swatch {
width: var(--onoffswitch-width) !important;
height: 20px !important;
}
.cm-colorview::before {
margin: 1px !important;
}
.color-swatch {
position: absolute;
border: 1px solid gray;
margin-top: -22px;
cursor: pointer;
}
.colorpicker-popup {
z-index: 2147483647 !important;
border: none !important;
box-shadow: 3px 3px 50px rgba(0,0,0,.5) !important;
}
@keyframes fadein {
from {
opacity: 0;
@ -826,7 +936,7 @@ fieldset > *:not(legend) {
}
.newUI .target {
max-width: calc(50vw - 60px);
max-width: calc(50vw - var(--actions-width));
}
}

View File

@ -3,6 +3,7 @@
/* global checkUpdate, handleUpdateInstalled */
/* global objectDiff */
/* global configDialog */
/* global initCollapsibles */
'use strict';
let installed;
@ -59,16 +60,6 @@ function initGlobalEvents() {
$('#manage-shortcuts-button').onclick = () => openURL({url: URLS.configureCommands});
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
// focus search field on / key
document.onkeypress = event => {
if ((event.keyCode || event.which) === 47
&& !event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey
&& !event.target.matches('[type="text"], [type="search"]')) {
event.preventDefault();
$('#search').focus();
}
};
// remember scroll position on normal history navigation
window.onbeforeunload = rememberScrollPosition;
@ -84,6 +75,9 @@ function initGlobalEvents() {
// N.B. triggers existing onchange listeners
setupLivePrefs();
// the options block
initCollapsibles();
$$('[id^="manage.newUI"]')
.forEach(el => (el.oninput = (el.onchange = switchUI)));
@ -185,7 +179,8 @@ function createStyleElement({style, name}) {
entry.styleMeta = getStyleWithNoCode(style);
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '');
(style.updateUrl ? ' updatable' : '') +
(style.usercssData ? ' usercss' : '');
if (style.url) {
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
@ -193,7 +188,7 @@ function createStyleElement({style, name}) {
if (style.updateUrl && newUI.enabled) {
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
}
if (shouldShowConfig() && newUI.enabled) {
if (style.usercssData && Object.keys(style.usercssData.vars).length > 0 && newUI.enabled) {
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
}
@ -202,10 +197,6 @@ function createStyleElement({style, name}) {
createStyleTargetsElement({entry, style, postponeFavicons: name});
return entry;
function shouldShowConfig() {
return style.usercssData && Object.keys(style.usercssData.vars).length > 0;
}
}
@ -277,7 +268,7 @@ Object.assign(handleEvent, {
ENTRY_ROUTES: {
'.checker, .enable, .disable': 'toggle',
'.style-name-link': 'edit',
'.style-name': 'edit',
'.homepage': 'external',
'.check-update': 'check',
'.update': 'update',
@ -286,24 +277,6 @@ Object.assign(handleEvent, {
'.configure-usercss': 'config'
},
config(event, {styleMeta: style}) {
configDialog(style).then(vars => {
if (!vars) {
return;
}
const keys = Object.keys(vars).filter(k => vars[k].dirty);
if (!keys.length) {
return;
}
style.reason = 'config';
for (const key of keys) {
style.usercssData.vars[key].value = vars[key].value;
}
onBackgroundReady()
.then(() => BG.usercssHelper.save(style));
});
},
entryClicked(event) {
const target = event.target;
const entry = target.closest('.entry');
@ -330,9 +303,9 @@ Object.assign(handleEvent, {
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const url = event.target.closest('[href]').href;
const url = $('[href]', event.target.closest('.entry')).href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (openWindow) {
if (chrome.windows && openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
} else {
openURL({url, active: openForegroundTab});
@ -406,6 +379,10 @@ Object.assign(handleEvent, {
}
}
},
config(event, {styleMeta}) {
configDialog(styleMeta);
},
});
@ -423,8 +400,8 @@ function handleUpdate(style, {reason, method} = {}) {
oldEntry.remove();
}
}
if (reason === 'update' && entry.matches('.updatable')) {
handleUpdateInstalled(entry);
if (reason === 'update' || reason === 'install' && entry.matches('.updatable')) {
handleUpdateInstalled(entry, reason);
}
filterAndAppend({entry});
if (!entry.matches('.hidden') && reason !== 'import') {
@ -546,10 +523,13 @@ function usePrefsDuringPageLoad() {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
// [naively] assuming each element of addedNodes is a childless element
const prefValue = node.id ? prefs.readOnlyValues[node.id] : undefined;
const key = node.dataset && node.dataset.pref || node.id;
const prefValue = key ? prefs.readOnlyValues[key] : undefined;
if (prefValue !== undefined) {
if (node.type === 'checkbox') {
node.checked = prefValue;
} else if (node.localName === 'details') {
node.open = prefValue;
} else {
node.value = prefValue;
}
@ -580,10 +560,10 @@ function dieOnNullBackground() {
if (!FIREFOX || BG) {
return;
}
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
sendMessage({method: 'healthCheck'}, health => {
if (health && !chrome.extension.getBackgroundPage()) {
onDOMready().then(() => {
chrome.runtime.sendMessage({method: 'getStyles'}, showStyles);
sendMessage({method: 'getStyles'}, showStyles);
messageBox({
title: 'Stylus',
className: 'danger center',

View File

@ -185,9 +185,12 @@ function showUpdateHistory() {
}
function handleUpdateInstalled(entry) {
entry.classList.add('update-done');
function handleUpdateInstalled(entry, reason) {
const isNew = reason === 'install';
const note = t(isNew ? 'installButtonInstalled' : 'updateCompleted');
entry.classList.add('update-done', ...(isNew ? ['install-done'] : []));
entry.classList.remove('can-update', 'updatable');
$('.update-note', entry).textContent = t('updateCompleted');
$('.update-note', entry).textContent = note;
$('.updated', entry).title = note;
renderUpdatesOnlyFilter();
}

View File

@ -1,6 +1,6 @@
{
"name": "Stylus",
"version": "1.1.5",
"version": "1.1.6.3",
"minimum_chrome_version": "49",
"description": "__MSG_description__",
"homepage_url": "http://add0n.com/stylus.html",
@ -54,14 +54,14 @@
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
"run_at": "document_start",
"all_frames": false,
"js": ["content/install.js"]
"js": ["content/install-hook-userstyles.js"]
},
{
"matches": ["<all_urls>"],
"include_globs": ["*.user.css", "*.user.styl"],
"run_at": "document_idle",
"all_frames": false,
"js": ["content/util.js", "content/install-user-css.js"]
"js": ["content/install-hook-usercss.js"]
}
],
"browser_action": {

View File

@ -15,6 +15,7 @@ function messageBox({
if (onshow) {
onshow(messageBox.element);
}
messageBox.element.focus();
return new Promise(_resolve => {
messageBox.resolve = _resolve;
});

View File

@ -1,7 +1,8 @@
<!DOCTYPE html>
<html id="stylus">
<head>
<meta charset="utf-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title i18n-text-append="optionsHeading">Stylus </title>
<link rel="stylesheet" href="options/options.css">
<link rel="stylesheet" href="options/onoffswitch.css">
@ -23,7 +24,7 @@
<script src="content/apply.js"></script>
</head>
<body>
<body id="stylus-options">
<div id="options">
<div class="block">
@ -56,7 +57,7 @@
<label>
<span i18n-text="prefShowBadge"></span>
<span class="onoffswitch">
<input type="checkbox" id="show-badge">
<input type="checkbox" id="show-badge" class="slider">
<span></span>
</span>
</label>
@ -82,21 +83,21 @@
<span i18n-text="popupOpenEditInWindow"
i18n-title="popupOpenEditInWindowTooltip"></span>
<span class="onoffswitch">
<input type="checkbox" id="openEditInWindow">
<input type="checkbox" id="openEditInWindow" class="slider">
<span></span>
</span>
</label>
<label>
<span i18n-text="popupStylesFirst"></span>
<span class="onoffswitch">
<input type="checkbox" id="popup.stylesFirst">
<input type="checkbox" id="popup.stylesFirst" class="slider">
<span></span>
</span>
</label>
<label class="chromium-only">
<span i18n-text="popupBorders" i18n-title="popupBordersTooltip"></span>
<span class="onoffswitch">
<input type="checkbox" id="popup.borders">
<input type="checkbox" id="popup.borders" class="slider">
<span></span>
</span>
</label>
@ -125,24 +126,17 @@
</h1>
</div>
<div class="items">
<label>
<span i18n-text="optionsAdvancedNewStyleAsUsercss"></span>
<span class="onoffswitch">
<input type="checkbox" id="newStyleAsUsercss">
<span></span>
</span>
</label>
<label>
<span i18n-text="optionsAdvancedExposeIframes"> <sup>2</sup></span>
<span class="onoffswitch">
<input type="checkbox" id="exposeIframes">
<input type="checkbox" id="exposeIframes" class="slider">
<span></span>
</span>
</label>
<label class="chromium-only">
<span i18n-text="optionsAdvancedContextDelete"></span>
<span class="onoffswitch">
<input type="checkbox" id="editor.contextDelete">
<input type="checkbox" id="editor.contextDelete" class="slider">
<span></span>
</span>
</label>

View File

@ -33,6 +33,11 @@ body {
}
}
.firefox body {
/* match the default FF theme */
background-color: #f9f9fa;
}
.firefox .chromium-only {
display: none;
}

View File

@ -1,7 +1,8 @@
<html id="stylus">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="popup/popup.css">
<link rel="stylesheet" href="popup/search-results.css">
@ -102,6 +103,7 @@
<script src="content/apply.js"></script>
<script src="popup/popup.js"></script>
<script src="popup/search-results.js"></script>
<script src="popup/hotkeys.js"></script>
</head>
<body id="stylus-popup">
@ -117,6 +119,8 @@
</div>
</div>
<aside id="hotkey-info" i18n-title="popupHotkeysTooltip"></aside>
<div id="installed"></div>
<div class="actions">

168
popup/hotkeys.js Normal file
View File

@ -0,0 +1,168 @@
/* global applyOnMessage installed */
'use strict';
window.addEventListener('showStyles:done', function _() {
window.removeEventListener('showStyles:done', _);
let togglablesShown = true;
let togglables = getTogglables();
window.addEventListener('keydown', onKeyDown);
initHotkeyInfo();
return;
function onKeyDown(event) {
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
let entry;
const k = event.which;
if (k >= 48 && k <= 57 || k >= 96 && k <= 105) {
// 0-9, numpad 0-9
entry = installed.children[k === 48 || k === 96 ? 9 : k - (k > 96 ? 97 : 49)];
} else if (k >= 65 && k <= 90) {
// a-z
const letter = new RegExp('^\\x' + k.toString(16), 'i');
entry = [...installed.children].find(entry => letter.test(entry.textContent));
} else if (k === 192 || k === 106) {
// backtick ` and numpad *
invertTogglables();
} else if (k === 109) {
// numpad -
toggleState(installed.children, 'enabled', false);
} else if (k === 107) {
// numpad +
toggleState(installed.children, 'disabled', true);
}
if (!entry) {
return;
}
const target = $(event.shiftKey ? '.style-edit-link' : '.checker', entry);
target.dispatchEvent(new MouseEvent('click'));
}
function getTogglables() {
const all = [...installed.children];
const enabled = [];
for (const entry of all) {
if (entry.classList.contains('enabled')) {
enabled.push(entry.id);
}
}
return enabled.length ? enabled : all.map(entry => entry.id);
}
function countEnabledTogglables() {
let num = 0;
for (const id of togglables) {
num += $(`#${id}`).classList.contains('enabled');
}
return num;
}
function invertTogglables() {
togglables = togglables.length ? togglables : getTogglables();
togglablesShown = countEnabledTogglables() > togglables.length / 2;
toggleState(togglables, null, !togglablesShown);
togglablesShown = !togglablesShown;
}
function toggleState(list, match, enable) {
const results = [];
let task = Promise.resolve();
for (let entry of list) {
entry = typeof entry === 'string' ? $('#' + entry) : entry;
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
results.push(entry.id);
task = task.then(() => saveStyleSafe({
id: entry.styleId,
enabled: enable,
notify: false,
}));
}
}
if (results.length) {
task.then(refreshAllTabs);
}
return results;
}
function refreshAllTabs() {
getStylesSafe({matchUrl: location.href, enabled: true, asHash: true})
.then(styles => applyOnMessage({method: 'styleReplaceAll', styles}));
queryTabs().then(tabs => {
for (const tab of tabs) {
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
if (!FIREFOX || tab.width) {
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
const message = {method: 'styleReplaceAll', styles, tabId: tab.id};
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
setTimeout(BG.updateIcon, 0, tab, styles);
});
}
}
});
}
function initHotkeyInfo() {
const container = $('#hotkey-info');
let title;
container.onclick = ({target}) => {
if (target.localName === 'button') {
close();
} else if (!container.dataset.active) {
open();
}
};
function close() {
delete container.dataset.active;
document.body.style.height = '';
container.title = title;
}
function open() {
title = container.title;
container.title = '';
container.dataset.active = true;
if (!container.firstElementChild) {
buildElement();
}
const height = 4 +
container.firstElementChild.scrollHeight +
container.lastElementChild.scrollHeight +
parseFloat(getComputedStyle(container.firstElementChild).paddingBottom) * 4;
if (height > document.body.clientHeight) {
document.body.style.height = height + 'px';
}
}
function buildElement() {
const keysToElements = line =>
line
.split(/(<.*?>)/)
.map(s => (!s.startsWith('<') ? s :
$element({tag: 'mark', textContent: s.slice(1, -1)})));
const linesToElements = text =>
text
.trim()
.split('\n')
.map((line, i, array) =>
$element(i < array.length - 1 ? {
tag: 'p',
appendChild: keysToElements(line),
} : {
tag: 'a',
target: '_blank',
href: 'https://github.com/openstyles/stylus/wiki/Popup',
textContent: line,
}));
[
linesToElements(t('popupHotkeysInfo')),
$element({tag: 'button', textContent: t('confirmOK')}),
].forEach(child => {
container.appendChild($element({appendChild: child}));
});
}
}
});

View File

@ -84,6 +84,7 @@ body.blocked > DIV {
padding-top: 2px;
max-height: 445px;
overflow-y: auto;
counter-reset: style-number;
}
#installed.disabled .style-name {
@ -110,12 +111,27 @@ body.blocked > DIV {
display: flex;
align-items: center;
padding: 5px 0.75em;
position: relative;
}
.entry:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
.entry:nth-child(-n+10):before,
.entry:nth-child(10):before {
counter-increment: style-number;
content: counter(style-number);
position: absolute;
top: .9ex;
right: 5px;
color: #aaa;
}
.entry:nth-child(10):before {
content: "0";
}
.entry .style-edit-link {
margin-right: 2px;
}
@ -223,6 +239,9 @@ a:hover .svg-icon {
body > .actions {
margin-top: 0.5em;
/* raise the actions above the hotkey-info */
position: relative;
z-index: 4;
}
.actions > div:not(:last-child):not(#disable-all-wrapper),
@ -431,6 +450,76 @@ body.blocked .actions > .left-gutter {
margin: 0;
}
/******************************************/
#hotkey-info {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 16px;
cursor: help;
margin: 0;
padding: 0;
z-index: 1;
hyphens: auto;
}
#hotkey-info:not([data-active]) > * {
display: none;
}
#hotkey-info[data-active] {
left: 6ex;
width: auto;
cursor: auto;
display: flex;
flex-direction: column;
border-left: 2px solid white;
box-shadow: 0 0 90px rgba(0, 0, 0, .5);
z-index: 5;
}
#hotkey-info div:first-child {
flex-grow: 1;
padding: 0 1em;
font-size: 11px;
overflow-y: auto;
}
#hotkey-info div {
padding: 1em;
border-top: 1px solid #ddd;
background-color: white;
}
#hotkey-info div:last-child {
box-shadow: 0 0 90px rgba(0, 0, 0, .25);
position: relative;
}
#hotkey-info p {
text-indent: -3px;
}
#hotkey-info p:last-child {
margin-bottom: 0;
}
#hotkey-info mark {
display: inline-block;
background: linear-gradient(#ccc, #fff);
padding: 1px 6px 0;
margin: 2px;
border: 1px solid white;
border-radius: 4px;
box-shadow: 1px 1px 4px rgba(0, 0, 0, .3);
font-weight: bold;
white-space: nowrap;
}
/******************************************/
@keyframes lights-off {
from {
background-color: transparent;

View File

@ -1,4 +1,3 @@
/* global retranslateCSS */
'use strict';
let installed;
@ -119,7 +118,7 @@ function initPopup(url) {
}
getActiveTab().then(function ping(tab, retryCountdown = 10) {
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => {
sendMessage({tabId: tab.id, method: 'ping', frameId: 0}, pong => {
if (pong) {
return;
}
@ -229,6 +228,7 @@ function showStyles(styles) {
});
}
}
window.dispatchEvent(new Event('showStyles:done'));
});
}
@ -267,6 +267,11 @@ function createStyleElement({
});
styleName.checkbox = checkbox;
styleName.appendChild(document.createTextNode(style.name));
setTimeout((el = styleName) => {
if (el.scrollWidth > el.clientWidth + 1) {
el.title = el.textContent;
}
});
$('.enable', entry).onclick = handleEvent.toggle;
$('.disable', entry).onclick = handleEvent.toggle;
@ -354,7 +359,7 @@ Object.assign(handleEvent, {
},
openLink(event) {
if (!prefs.get('openEditInWindow', false)) {
if (!chrome.windows || !prefs.get('openEditInWindow', false)) {
handleEvent.openURLandHide.call(this, event);
return;
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 jinho park (cyberuls@gmail.com, easylogic)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,387 @@
/* codemirror colorview */
.cm-colorview {
position: relative;
white-space: nowrap;
}
.cm-colorview:not(.cm-colorview-disabled)::before {
content: "";
position: relative;
display: inline-block;
box-sizing: content-box;
margin: 0 3px;
width: 8px;
height: 8px;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
background-repeat: repeat;
}
.CodeMirror-lint-mark-warning + .cm-colorview::before,
.cm-colorview-next-disabled + .cm-colorview::before {
content: none;
}
.codemirror-colorview-background {
position: absolute;
left: 2px;
top: 2px;
width: 10px;
height: 10px;
box-sizing: border-box;
border: 1px solid #8e8e8e;
content: "";
cursor: pointer;
}
.codemirror-colorview-background:hover {
border-color: #494949;
}
/* colorpicker */
.colorpicker-theme-light {
--main-background-color: #fff;
--main-border-color: #ccc;
--label-color: #666;
--label-color-hover: #000;
--input-background-color: #fff;
--input-background-color-hover: #ddd;
--input-background-color-focus: #fff;
--input-color: #444;
--input-color-focus: #000;
--input-border-color: #bbb;
--input-border-color-focus: #888;
--input-border-color-hover: #444;
--invalid-border-color: hsl(0, 100%, 50%);
--invalid-background-color: hsla(0, 100%, 50%, 0.15);
--invalid-color: hsl(0, 100%, 40%);
}
.colorpicker-theme-dark {
--main-background-color: #242424;
--main-border-color: #888;
--label-color: #aaa;
--label-color-hover: #eee;
--input-background-color: #222;
--input-background-color-hover: #222;
--input-background-color-focus: #383838;
--input-color: #ddd;
--input-color-focus: #fff;
--input-border-color: #505050;
--input-border-color-focus: #777;
--input-border-color-hover: #888;
--invalid-border-color: hsl(0, 100%, 27%);
--invalid-background-color: hsla(0, 100%, 50%, 0.3);
--invalid-color: hsl(0, 100%, 75%);
}
.colorpicker-popup {
--switcher-width: 30px;
position: relative;
width: 350px;
z-index: 1000;
transition: opacity .5s;
color: var(--label-color);
border: 1px solid var(--main-border-color);
background-color: var(--main-background-color);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.colorpicker-popup[data-fading="1"] {
opacity: .75;
}
.colorpicker-popup[data-fading="2"] {
opacity: 0;
}
.colorpicker-saturation-container {
position: relative;
height: 120px;
overflow: hidden;
cursor: pointer;
}
.colorpicker-opacity-bar {
position: absolute;
display: block;
content: "";
left: 0;
right: 0;
bottom: 0;
top: 0;
background: linear-gradient(to right, rgba(232, 232, 232, 0), rgba(232, 232, 232, 1));
}
.colorpicker-saturation {
position: relative;
width: 100%;
height: 100%;
background-color: rgba(204, 154, 129, 0);
background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
background-repeat: repeat-x;
}
.colorpicker-value {
position: relative;
width: 100%;
height: 100%;
background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
}
.colorpicker-drag-pointer {
position: absolute;
width: 10px;
height: 10px;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
left: -5px;
top: -5px;
border: 1px solid #fff;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05);
}
.colorpicker-sliders {
position: relative;
padding: 10px 0 6px 0;
border-top: 1px solid transparent;
}
.colorpicker-theme-dark .colorpicker-sliders {
border-color: var(--input-border-color);
}
.colorpicker-swatch,
.colorpicker-empty {
position: absolute;
left: 11px;
top: 17px;
width: 30px;
height: 30px;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.colorpicker-empty {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC") repeat;
}
.colorpicker-hue {
position: relative;
padding: 6px 12px;
margin: 0 0 0 45px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.colorpicker-hue-container {
position: relative;
width: 100%;
height: 10px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
}
.colorpicker-opacity {
position: relative;
padding: 3px 12px;
margin: 0 0 0 45px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.colorpicker-opacity-container {
position: relative;
width: 100%;
height: 10px;
z-index: 2;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
background-repeat: repeat;
}
.colorpicker-hue-knob,
.colorpicker-opacity-knob {
position: absolute;
cursor: pointer;
top: 50% !important;
margin-top: -7px !important;
left: -3px;
width: 12px;
height: 12px;
-webkit-border-radius: 50px;
-moz-border-radius: 50px;
border-radius: 50px;
border: 1px solid rgba(0, 0, 0, 0.5);
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.colorpicker-input-container {
position: relative;
-webkit-box-sizing: padding-box;
-moz-box-sizing: padding-box;
box-sizing: padding-box;
}
.colorpicker-input-group {
display: none;
position: relative;
padding: 0 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin-right: calc(var(--switcher-width) - 10px);
}
.colorpicker-input-group[data-active] {
display: flex;
}
.colorpicker-input-field {
display: block;
position: relative;
flex: 1;
padding: 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.colorpicker-input-field[class$="-a"] {
flex-grow: 1.5;
}
.colorpicker-hsl-h::before {
content: "\b0"; /* degree */
position: absolute;
right: -2px;
top: 8px;
}
.colorpicker-hsl-s::before,
.colorpicker-hsl-l::before {
content: "%";
position: absolute;
right: -1ex;
top: 8px;
font-size: 10px;
}
.colorpicker-input {
text-align: center;
width: 100%;
padding: 3px 5px;
font-size: 11px;
font-weight: bold;
box-sizing: border-box;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
-o-user-select: text;
user-select: text;
border: 1px solid var(--input-border-color);
background-color: var(--input-background-color);
color: var(--input-color);
}
.colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button {
-webkit-filter: invert(1);
filter: invert(1);
}
.colorpicker-input:hover {
border-color: var(--input-border-color-hover);
}
.colorpicker-input:focus {
color: var(--input-color-focus);
border-color: var(--input-border-color-focus);
background-color: var(--input-background-color-focus);
}
.colorpicker-theme-dark input:focus {
outline: none !important;
}
.colorpicker-input:invalid {
border-color: var(--invalid-border-color);
background-color: var(--invalid-background-color);
color: var(--invalid-color);
}
.colorpicker-title {
text-align: center;
font-size: 12px;
font-family: monospace;
display: flex;
justify-content: center;
color: var(--label-color);
}
.colorpicker-title-action {
cursor: pointer;
}
.colorpicker-title-action[data-active] {
font-weight: bold;
color: var(--input-color);
cursor: default;
pointer-events: none;
}
.colorpicker-format-change {
position: absolute;
display: block;
width: var(--switcher-width);
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.colorpicker-format-change-button {
width: 100%;
height: 100%;
background: transparent;
border: 0;
cursor: pointer;
outline: none;
font-family: monospace !important;
font-size: var(--switcher-width) !important;
margin-top: -5px;
color: var(--label-color);
text-align: center;
}
.colorpicker-format-change-button:hover {
color: var(--label-color-hover);
}

View File

@ -0,0 +1,970 @@
/* global CodeMirror */
'use strict';
(window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
const cm = this;
const CSS_PREFIX = 'colorpicker-';
const HUE_COLORS = [
{hex: '#ff0000', start: .0},
{hex: '#ffff00', start: .17},
{hex: '#00ff00', start: .33},
{hex: '#00ffff', start: .50},
{hex: '#0000ff', start: .67},
{hex: '#ff00ff', start: .83},
{hex: '#ff0000', start: 1}
];
let HSV = {};
let currentFormat;
let initialized = false;
let shown = false;
let options = {};
let $root;
let $sat, $satPointer;
let $hue, $hueKnob;
let $opacity, $opacityBar, $opacityKnob;
let $swatch;
let $formatChangeButton;
let $hexCode;
const $inputGroups = {};
const $inputs = {};
const $rgb = {};
const $hsl = {};
const $hexLettercase = {};
const allowInputFocus = !('ontouchstart' in document) || window.innerHeight > 800;
const dragging = {
saturationPointerPos: {x: 0, y: 0},
hueKnobPos: 0,
saturation: false,
hue: false,
opacity: false,
};
let prevFocusedElement;
let lastOutputColor;
let userActivity;
let timerCloseColorPicker;
let timerFadeColorPicker;
const PUBLIC_API = {
$root,
show,
hide,
setColor,
getColor,
options,
};
return PUBLIC_API;
//region DOM
function init() {
// simplified createElement
function $(a, b) {
const cls = typeof a === 'string' || Array.isArray(a) ? a : '';
const props = b || a;
const {tag = 'div', children} = props || {};
const el = document.createElement(tag);
el.className = (Array.isArray(cls) ? cls : [cls])
.map(c => (c ? CSS_PREFIX + c : ''))
.join(' ');
if (!props) {
return el;
}
for (const child of Array.isArray(children) ? children : [children]) {
if (child) {
el.appendChild(child instanceof Node ? child : document.createTextNode(child));
}
}
delete props.tag;
delete props.children;
return Object.assign(el, props);
}
const alphaPattern = /^\s*(0+\.?|0*\.\d+|0*1\.?|0*1\.0*)?\s*$/.source;
$root = $('popup', {children: [
$sat = $('saturation-container', {children: [
$('saturation', {children: [
$('value', {children: [
$satPointer = $('drag-pointer'),
]}),
]}),
]}),
$('sliders', {children: [
$('hue', {children: [
$hue = $('hue-container', {children: [
$hueKnob = $('hue-knob'),
]}),
]}),
$('opacity', {children: [
$opacity = $('opacity-container', {children: [
$opacityBar = $('opacity-bar'),
$opacityKnob = $('opacity-knob'),
]}),
]}),
$('empty'),
$swatch = $('swatch'),
]}),
$(['input-container', 'hex'], {children: [
$inputGroups.hex = $(['input-group', 'hex'], {children: [
$(['input-field', 'hex'], {children: [
$hexCode = $('input', {tag: 'input', type: 'text', spellcheck: false,
pattern: /^\s*#([a-fA-F\d]{3}([a-fA-F\d]([a-fA-F\d]{2}([a-fA-F\d]{2})?)?)?)\s*$/.source
}),
$('title', {children: [
$hexLettercase.true = $('title-action', {textContent: 'HEX'}),
'\xA0/\xA0',
$hexLettercase.false = $('title-action', {textContent: 'hex'}),
]}),
]}),
]}),
$inputGroups.rgb = $(['input-group', 'rgb'], {children: [
$(['input-field', 'rgb-r'], {children: [
$rgb.r = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
$('title', {textContent: 'R'}),
]}),
$(['input-field', 'rgb-g'], {children: [
$rgb.g = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
$('title', {textContent: 'G'}),
]}),
$(['input-field', 'rgb-b'], {children: [
$rgb.b = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
$('title', {textContent: 'B'}),
]}),
$(['input-field', 'rgb-a'], {children: [
$rgb.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
$('title', {textContent: 'A'}),
]}),
]}),
$inputGroups.hsl = $(['input-group', 'hsl'], {children: [
$(['input-field', 'hsl-h'], {children: [
$hsl.h = $('input', {tag: 'input', type: 'number', step: 1}),
$('title', {textContent: 'H'}),
]}),
$(['input-field', 'hsl-s'], {children: [
$hsl.s = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}),
$('title', {textContent: 'S'}),
]}),
$(['input-field', 'hsl-l'], {children: [
$hsl.l = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}),
$('title', {textContent: 'L'}),
]}),
$(['input-field', 'hsl-a'], {children: [
$hsl.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
$('title', {textContent: 'A'}),
]}),
]}),
$('format-change', {children: [
$formatChangeButton = $('format-change-button', {textContent: '↔'}),
]}),
]}),
]});
$inputs.hex = [$hexCode];
$inputs.rgb = [$rgb.r, $rgb.g, $rgb.b, $rgb.a];
$inputs.hsl = [$hsl.h, $hsl.s, $hsl.l, $hsl.a];
const inputsToArray = inputs => inputs.map(el => parseFloat(el.value));
const inputsToHexString = () => $hexCode.value.trim();
const inputsToRGB = ([r, g, b, a] = inputsToArray($inputs.rgb)) => ({r, g, b, a, type: 'rgb'});
const inputsToHSL = ([h, s, l, a] = inputsToArray($inputs.hsl)) => ({h, s, l, a, type: 'hsl'});
Object.defineProperty($inputs.hex, 'color', {get: inputsToHexString});
Object.defineProperty($inputs.rgb, 'color', {get: inputsToRGB});
Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL});
Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color});
HUE_COLORS.forEach(color => Object.assign(color, stringToColor(color.hex)));
initialized = true;
}
//endregion
//region Public API
function show(opt) {
if (!initialized) {
init();
}
HSV = {};
currentFormat = '';
options = PUBLIC_API.options = opt;
prevFocusedElement = document.activeElement;
userActivity = 0;
lastOutputColor = opt.color || '';
$formatChangeButton.title = opt.tooltipForSwitcher || '';
opt.hideDelay = Math.max(0, opt.hideDelay) || 2000;
if (!isNaN(options.left) && !isNaN(options.top)) {
$root.style = `
display: block;
position: fixed;
`.replace(/;/g, '!important;');
reposition();
}
$root.classList.add(CSS_PREFIX + 'theme-' +
(opt.theme === 'dark' || opt.theme === 'light' ?
opt.theme :
guessTheme()));
document.body.appendChild($root);
shown = true;
registerEvents();
setFromColor(opt.color);
setFromHexLettercaseElement();
}
function hide() {
if (shown) {
unregisterEvents();
focusNoScroll(prevFocusedElement);
$root.remove();
shown = false;
}
}
function setColor(color) {
switch (typeof color) {
case 'string':
color = stringToColor(color);
break;
case 'object': {
const {r, g, b, a} = color;
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
color = {r, g, b, a, type: 'rgb'};
break;
}
const {h, s, l} = color;
if (!isNaN(h) && !isNaN(s) && !isNaN(l)) {
color = {h, s, l, a, type: 'hsl'};
break;
}
}
// fallthrough
default:
return false;
}
if (color) {
if (!initialized) {
init();
}
setFromColor(color);
}
return Boolean(color);
}
function getColor(type) {
if (!initialized) {
return;
}
readCurrentColorFromRamps();
const color = type === 'hsl' ? HSVtoHSL(HSV) : HSVtoRGB(HSV);
return type ? colorToString(color, type) : color;
}
//endregion
//region DOM-to-state
function readCurrentColorFromRamps() {
if ($sat.offsetWidth === 0) {
HSV.h = HSV.s = HSV.v = 0;
} else {
const {x, y} = dragging.saturationPointerPos;
HSV.h = snapToInt((dragging.hueKnobPos / $hue.offsetWidth) * 360);
HSV.s = x / $sat.offsetWidth;
HSV.v = ($sat.offsetHeight - y) / $sat.offsetHeight;
}
}
function setFromSaturationElement(event) {
event.preventDefault();
const w = $sat.offsetWidth;
const h = $sat.offsetHeight;
const deltaX = event.clientX - parseFloat($root.style.left);
const deltaY = event.clientY - parseFloat($root.style.top);
const x = dragging.saturationPointerPos.x = constrain(0, w, deltaX);
const y = dragging.saturationPointerPos.y = constrain(0, h, deltaY);
$satPointer.style.left = `${x - 5}px`;
$satPointer.style.top = `${y - 5}px`;
readCurrentColorFromRamps();
renderInputs();
}
function setFromHueElement(event) {
const {left, width} = getScreenBounds($hue);
const currentX = event ? getTouchPosition(event).clientX :
left + width * constrainHue(HSV.h) / 360;
const normalizedH = constrain(0, 1, (currentX - left) / width);
const x = dragging.hueKnobPos = width * normalizedH;
$hueKnob.style.left = (x - Math.round($hueKnob.offsetWidth / 2)) + 'px';
$sat.style.backgroundColor = hueDistanceToColorString(normalizedH);
HSV.h = event ? Math.round(normalizedH * 360) : HSV.h;
renderInputs();
}
function setFromOpacityElement(event) {
const {left, width} = getScreenBounds($opacity);
const normalized = constrain(0, 1, (getTouchPosition(event).clientX - left) / width);
const x = width * normalized;
$opacityKnob.style.left = (x - Math.ceil($opacityKnob.offsetWidth / 2)) + 'px';
HSV.a = Math.round(normalized * 100) / 100;
renderInputs();
}
function setFromFormatElement({shiftKey}) {
userActivity = performance.now();
HSV.a = isNaN(HSV.a) ? 1 : HSV.a;
const formats = ['hex', 'rgb', 'hsl'];
const dir = shiftKey ? -1 : 1;
const total = formats.length;
switchInputGroup(formats[(formats.indexOf(currentFormat) + dir + total) % total]);
renderInputs();
}
function setFromHexLettercaseElement() {
const isUpper = Boolean(options.hexUppercase);
$hexLettercase[isUpper].dataset.active = '';
delete $hexLettercase[!isUpper].dataset.active;
const value = $hexCode.value;
$hexCode.value = isUpper ? value.toUpperCase() : value.toLowerCase();
setFromInputs();
}
function setFromInputs(event) {
userActivity = event ? performance.now() : userActivity;
if ($inputs[currentFormat].every(validateInput)) {
setFromColor($inputs.color);
}
}
function setFromKeyboard(event) {
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
switch (which) {
case 9: // Tab
case 33: // PgUp
case 34: // PgDn
if (!ctrl && !alt && !meta) {
const el = document.activeElement;
const inputs = $inputs[currentFormat];
const lastInput = inputs[inputs.length - 1];
if (which === 9 && shift && el === inputs[0]) {
maybeFocus(lastInput);
} else if (which === 9 && !shift && el === lastInput) {
maybeFocus(inputs[0]);
} else if (which !== 9 && !shift) {
setFromFormatElement({shift: which === 33 || shift});
} else {
return;
}
event.preventDefault();
}
return;
case 38: // Up
case 40: // Down
if (!event.metaKey &&
document.activeElement.localName === 'input' &&
document.activeElement.checkValidity()) {
setFromKeyboardIncrement(event);
}
return;
}
}
function setFromKeyboardIncrement(event) {
const el = document.activeElement;
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
const dir = which === 38 ? 1 : -1;
let value, newValue;
if (currentFormat === 'hex') {
value = el.value.trim();
const isShort = value.length <= 5;
const [r, g, b, a = ''] = el.value.match(isShort ? /[\da-f]/gi : /[\da-f]{2}/gi);
let ceiling, data;
if (!ctrl && !shift && !alt) {
ceiling = isShort ? 0xFFF : 0xFFFFFF;
data = [[true, r + g + b]];
} else {
ceiling = isShort ? 15 : 255;
data = [[ctrl, r], [shift, g], [alt, b]];
}
newValue = '#' + data.map(([affected, part]) => {
part = constrain(0, ceiling, parseInt(part, 16) + dir * (affected ? 1 : 0));
return (part + ceiling + 1).toString(16).slice(1);
}).join('') + a;
newValue = options.hexUppercase ? newValue.toUpperCase() : newValue.toLowerCase();
} else if (!alt) {
value = parseFloat(el.value);
const isHue = el === $inputs.hsl[0];
const isAlpha = el === $inputs[currentFormat][3];
const isRGB = currentFormat === 'rgb';
const min = isHue ? -360 : 0;
const max = isHue ? 360 : isAlpha ? 1 : isRGB ? 255 : 100;
const scale = isAlpha ? .01 : 1;
const delta =
shift && !ctrl ? 10 :
ctrl && !shift ? (isHue || isRGB ? 100 : 50) :
1;
newValue = constrain(min, max, value + delta * scale * dir);
newValue = isAlpha ? alphaToString(newValue) : newValue;
}
event.preventDefault();
userActivity = performance.now();
if (newValue !== undefined && newValue !== value) {
el.value = newValue;
setFromColor($inputs.color);
}
}
function validateInput(el) {
const isAlpha = el === $inputs[currentFormat][3];
let isValid = (isAlpha || el.value.trim()) && el.checkValidity();
if (!isAlpha && !isValid && currentFormat === 'rgb') {
isValid = parseAs(el, parseInt);
} else if (isAlpha && !isValid) {
isValid = parseAs(el, parseFloat);
}
if (isAlpha && isValid) {
isValid = lastOutputColor !== colorToString($inputs.color);
}
return isValid;
}
//endregion
//region State-to-DOM
function setFromColor(color) {
color = typeof color === 'string' ? stringToColor(color) : color;
color = color || stringToColor('#f00');
const newHSV = color.type === 'hsl' ? HSLtoHSV(color) : RGBtoHSV(color);
if (Object.keys(newHSV).every(k => Math.abs(newHSV[k] - HSV[k]) < 1e-3)) {
return;
}
HSV = newHSV;
renderKnobs(color);
switchInputGroup(color.type);
setFromHueElement();
}
function switchInputGroup(format) {
if (currentFormat === format) {
return;
}
if (currentFormat) {
delete $inputGroups[currentFormat].dataset.active;
} else {
for (const format in $inputGroups) {
delete $inputGroups[format].dataset.active;
}
}
$inputGroups[format].dataset.active = '';
maybeFocus($inputs[format][0]);
currentFormat = format;
}
function renderKnobs(color) {
const x = $sat.offsetWidth * HSV.s;
const y = $sat.offsetHeight * (1 - HSV.v);
$satPointer.style.left = (x - 5) + 'px';
$satPointer.style.top = (y - 5) + 'px';
dragging.saturationPointerPos = {x, y};
const hueX = $hue.offsetWidth * constrain(0, 1, HSV.h / 360);
$hueKnob.style.left = (hueX - 7.5) + 'px';
dragging.hueKnobPos = hueX;
const opacityX = $opacity.offsetWidth * (isNaN(HSV.a) ? 1 : HSV.a);
$opacityKnob.style.left = (opacityX - 7.5) + 'px';
$sat.style.backgroundColor = color;
}
function renderInputs() {
const rgb = HSVtoRGB(HSV);
switch (currentFormat) {
case 'hex':
$hexCode.value = colorToString(rgb, 'hex');
break;
case 'rgb': {
$rgb.r.value = rgb.r;
$rgb.g.value = rgb.g;
$rgb.b.value = rgb.b;
$rgb.a.value = alphaToString() || 1;
break;
}
case 'hsl': {
const {h, s, l} = HSVtoHSL(HSV);
$hsl.h.value = h;
$hsl.s.value = s;
$hsl.l.value = l;
$hsl.a.value = alphaToString() || 1;
}
}
$swatch.style.backgroundColor = colorToString(rgb, 'rgb');
$opacityBar.style.background = 'linear-gradient(to right,' +
colorToString(Object.assign(rgb, {a: 0}), 'rgb') + ',' +
colorToString(Object.assign(rgb, {a: 1}), 'rgb') + ')';
colorpickerCallback();
}
//endregion
//region Event listeners
function onHexLettercaseClicked() {
options.hexUppercase = !options.hexUppercase;
setFromHexLettercaseElement();
}
function onSaturationMouseDown(event) {
if (captureMouse(event, 'saturation')) {
setFromSaturationElement(event);
}
}
function onSaturationMouseUp(event) {
releaseMouse(event, 'saturation');
}
function onHueKnobMouseDown(event) {
captureMouse(event, 'hue');
}
function onOpacityKnobMouseDown(event) {
captureMouse(event, 'opacity');
}
function onHueMouseDown(event) {
if (captureMouse(event, 'hue')) {
setFromHueElement(event);
}
}
function onOpacityMouseDown(event) {
if (captureMouse(event, 'opacity')) {
setFromOpacityElement(event);
}
}
function onMouseUp(event) {
if (releaseMouse(event, ['saturation', 'hue', 'opacity']) &&
!event.target.closest('.codemirror-colorview, .colorpicker-popup, .CodeMirror')) {
hide();
}
}
function onMouseMove(event) {
if (event.button !== 0) {
return;
}
if (dragging.saturation) {
setFromSaturationElement(event);
} else if (dragging.hue) {
setFromHueElement(event);
} else if (dragging.opacity) {
setFromOpacityElement(event);
}
}
function stopSnoozing() {
clearTimeout(timerCloseColorPicker);
clearTimeout(timerFadeColorPicker);
if ($root.dataset.fading) {
delete $root.dataset.fading;
}
}
function snooze() {
clearTimeout(timerFadeColorPicker);
timerFadeColorPicker = setTimeout(fade, options.hideDelay / 2);
}
function onKeyDown(e) {
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
switch (e.which) {
case 13:
setFromInputs({});
// fallthrough to 27
case 27:
colorpickerCallback(e.which === 27 ? '' : undefined);
e.preventDefault();
e.stopPropagation();
hide();
break;
}
}
}
function onCloseRequest(event) {
if (event.detail !== PUBLIC_API) {
hide();
}
}
//endregion
//region Event utilities
function colorpickerCallback(colorString = currentColorToString()) {
// Esc pressed?
if (!colorString) {
options.callback('');
}
if (
userActivity &&
$inputs[currentFormat].every(el => el.checkValidity()) &&
typeof options.callback === 'function'
) {
lastOutputColor = colorString.replace(/\b0\./g, '.');
options.callback(lastOutputColor);
}
}
function captureMouse({button}, mode) {
if (button !== 0) {
return;
}
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousemove', onMouseMove);
if (!mode) {
return;
}
for (const m of (Array.isArray(mode) ? mode : [mode])) {
dragging[m] = true;
}
userActivity = performance.now();
return true;
}
function releaseMouse(event, mode) {
if (event && event.button !== 0) {
return;
}
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousemove', onMouseMove);
if (!mode) {
return;
}
for (const m of (Array.isArray(mode) ? mode : [mode])) {
dragging[m] = false;
}
userActivity = performance.now();
return true;
}
function getTouchPosition(event) {
return event.touches && event.touches[0] || event;
}
function registerEvents() {
window.addEventListener('keydown', onKeyDown, true);
window.addEventListener('close-colorpicker-popup', onCloseRequest, true);
$root.addEventListener('mouseleave', snooze);
$root.addEventListener('mouseenter', stopSnoozing);
$root.addEventListener('input', setFromInputs);
$root.addEventListener('keydown', setFromKeyboard);
$formatChangeButton.addEventListener('click', setFromFormatElement);
$sat.addEventListener('mousedown', onSaturationMouseDown);
$sat.addEventListener('mouseup', onSaturationMouseUp);
$hueKnob.addEventListener('mousedown', onHueKnobMouseDown);
$opacityKnob.addEventListener('mousedown', onOpacityKnobMouseDown);
$hue.addEventListener('mousedown', onHueMouseDown);
$opacity.addEventListener('mousedown', onOpacityMouseDown);
$hexLettercase.true.addEventListener('click', onHexLettercaseClicked);
$hexLettercase.false.addEventListener('click', onHexLettercaseClicked);
stopSnoozing();
timerFadeColorPicker = setTimeout(fade, options.hideDelay / 2);
}
function unregisterEvents() {
window.removeEventListener('keydown', onKeyDown, true);
window.removeEventListener('close-colorpicker-popup', hide, true);
$root.removeEventListener('mouseleave', snooze);
$root.removeEventListener('mouseenter', stopSnoozing);
$root.removeEventListener('input', setFromInputs);
$formatChangeButton.removeEventListener('click', setFromFormatElement);
$sat.removeEventListener('mousedown', onSaturationMouseDown);
$sat.removeEventListener('mouseup', onSaturationMouseUp);
$hueKnob.removeEventListener('mousedown', onHueKnobMouseDown);
$opacityKnob.removeEventListener('mousedown', onOpacityKnobMouseDown);
$hue.removeEventListener('mousedown', onHueMouseDown);
$opacity.removeEventListener('mousedown', onOpacityMouseDown);
$hexLettercase.true.removeEventListener('click', onHexLettercaseClicked);
$hexLettercase.false.removeEventListener('click', onHexLettercaseClicked);
releaseMouse();
stopSnoozing();
}
//endregion
//region Color conversion utilities
function colorToString({r, g, b, h, s, l, a}, type = currentFormat) {
a = alphaToString(a);
const hasA = Boolean(a);
switch (type) {
case 'hex': {
const rgbStr = (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).slice(1);
const aStr = hasA ? (0x100 + Math.round(a * 255)).toString(16).slice(1) : '';
const hexStr = `#${rgbStr + aStr}`.replace(/^#(.)\1(.)\2(.)\3(?:(.)\4)?$/, '#$1$2$3$4');
return options.hexUppercase ? hexStr.toUpperCase() : hexStr.toLowerCase();
}
case 'rgb':
return hasA ?
`rgba(${r}, ${g}, ${b}, ${a})` :
`rgb(${r}, ${g}, ${b})`;
case 'hsl':
return hasA ?
`hsla(${h}, ${s}%, ${l}%, ${a})` :
`hsl(${h}, ${s}%, ${l}%)`;
}
}
function stringToColor(str) {
if (typeof str !== 'string') {
return;
}
str = str.trim();
if (str.startsWith('rgb')) {
const [r, g, b, a = 1] = str.replace(/rgba?\(|\)/g, '').split(',').map(parseFloat);
return {type: 'rgb', r, g, b, a};
}
if (str.startsWith('hsl')) {
const [h, s, l, a = 1] = str.replace(/hsla?\(|\)/g, '').split(',').map(parseFloat);
return {type: 'hsl', h, s, l, a};
}
if (str.startsWith('#')) {
str = str.slice(1);
const [r, g, b, a = 255] = str.length <= 4 ?
str.match(/(.)/g).map(c => parseInt(c + c, 16)) :
str.match(/(..)/g).map(c => parseInt(c, 16));
return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255};
}
}
function constrainHue(h) {
return h < 0 ? h % 360 + 360 :
h > 360 ? h % 360 :
h;
}
function RGBtoHSV({r, g, b, a}) {
r /= 255;
g /= 255;
b /= 255;
const MaxC = Math.max(r, g, b);
const MinC = Math.min(r, g, b);
const DeltaC = MaxC - MinC;
let h =
DeltaC === 0 ? 0 :
MaxC === r ? 60 * (((g - b) / DeltaC) % 6) :
MaxC === g ? 60 * (((b - r) / DeltaC) + 2) :
MaxC === b ? 60 * (((r - g) / DeltaC) + 4) :
0;
h = constrainHue(h);
return {
h,
s: MaxC === 0 ? 0 : DeltaC / MaxC,
v: MaxC,
a,
};
}
function HSVtoRGB({h, s, v}) {
h = constrainHue(h) % 360;
const C = s * v;
const X = C * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - C;
const [r, g, b] =
h >= 0 && h < 60 ? [C, X, 0] :
h >= 60 && h < 120 ? [X, C, 0] :
h >= 120 && h < 180 ? [0, C, X] :
h >= 180 && h < 240 ? [0, X, C] :
h >= 240 && h < 300 ? [X, 0, C] :
h >= 300 && h < 360 ? [C, 0, X] : [];
return {
r: snapToInt(Math.round((r + m) * 255)),
g: snapToInt(Math.round((g + m) * 255)),
b: snapToInt(Math.round((b + m) * 255)),
};
}
function HSLtoHSV({h, s, l, a}) {
const t = s * (l < 50 ? l : 100 - l) / 100;
return {
h: constrainHue(h),
s: t + l ? 200 * t / (t + l) / 100 : 0,
v: (t + l) / 100,
a,
};
}
function HSVtoHSL({h, s, v}) {
const l = (2 - s) * v / 2;
const t = l < .5 ? l * 2 : 2 - l * 2;
return {
h: Math.round(constrainHue(h)),
s: Math.round(t ? s * v / t * 100 : 0),
l: Math.round(l * 100),
};
}
function currentColorToString(format = currentFormat, alpha = HSV.a) {
const converted = format === 'hsl' ? HSVtoHSL(HSV) : HSVtoRGB(HSV);
converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha;
return colorToString(converted, format);
}
function mixColorToString(start, end, amount) {
const obj = {
r: start.r + (end.r - start.r) * amount,
g: start.g + (end.g - start.g) * amount,
b: start.b + (end.b - start.b) * amount,
a: 1,
};
return colorToString(obj, 'hex');
}
function hueDistanceToColorString(hueRatio) {
let prevColor;
for (const color of HUE_COLORS) {
if (prevColor && color.start >= hueRatio) {
return mixColorToString(prevColor, color,
(hueRatio - prevColor.start) / (color.start - prevColor.start));
}
prevColor = color;
}
return HUE_COLORS[0].hex;
}
function alphaToString(a = HSV.a) {
return isNaN(a) ? '' : (a + .5e-6).toFixed(7).slice(0, -1).replace(/^0(?=\.[1-9])|^1\.0+?$|\.?0+$/g, '');
}
//endregion
//region Miscellaneous utilities
function reposition() {
const width = $root.offsetWidth;
const height = $root.offsetHeight;
// set left position for color picker
let elementScreenLeft = options.left - document.scrollingElement.scrollLeft;
const bodyWidth = document.scrollingElement.scrollWidth;
if (width + elementScreenLeft > bodyWidth) {
elementScreenLeft -= (width + elementScreenLeft) - bodyWidth;
}
if (elementScreenLeft < 0) {
elementScreenLeft = 0;
}
// set top position for color picker
let elementScreenTop = options.top - document.scrollingElement.scrollTop;
if (height + elementScreenTop > window.innerHeight) {
elementScreenTop = window.innerHeight - height;
}
if (elementScreenTop < options.top) {
elementScreenTop = options.top - height - 20;
}
if (elementScreenTop < 0) {
elementScreenTop = 0;
}
// set position
$root.style.left = elementScreenLeft + 'px';
$root.style.top = elementScreenTop + 'px';
}
function fade({fadingStage = 1} = {}) {
const timeInactive = performance.now() - userActivity;
const delay = options.hideDelay / 2;
if (userActivity && timeInactive < delay) {
timerFadeColorPicker = setTimeout(fade, delay - timeInactive, 2);
clearTimeout(timerCloseColorPicker);
delete $root.dataset.fading;
return;
}
$root.dataset.fading = fadingStage;
if (fadingStage === 1) {
timerFadeColorPicker = setTimeout(fade, Math.max(0, delay - 500), {fadingStage: 2});
} else {
timerCloseColorPicker = setTimeout(hide, 500);
}
}
function maybeFocus(el) {
if (allowInputFocus) {
el.focus();
}
}
function focusNoScroll(el) {
if (el) {
const {scrollY: y, scrollX: x} = window;
el.focus({preventScroll: true});
el = null;
if (window.scrollY !== y || window.scrollX !== x) {
window.scrollTo(x, y);
}
}
}
function getScreenBounds(el) {
const bounds = el.getBoundingClientRect();
const {scrollTop, scrollLeft} = document.scrollingElement;
return {
top: bounds.top + scrollTop,
left: bounds.left + scrollLeft,
width: bounds.width,
height: bounds.height,
};
}
function guessTheme() {
const realColor = {r: 255, g: 255, b: 255, a: 1};
const start = options.guessBrightness ||
((cm.display.renderedView || [])[0] || {}).text || cm.display.lineDiv;
for (let el = start; el; el = el.parentElement) {
const bgColor = getComputedStyle(el).backgroundColor;
const [r, g, b, a = 255] = bgColor.match(/\d+/g).map(Number);
if (!a) {
continue;
}
const mixedA = 1 - (1 - a / 255) * (1 - realColor.a);
const q1 = a / 255 / mixedA;
const q2 = realColor.a * (1 - mixedA) / mixedA;
realColor.r = Math.round(r * q1 + realColor.r * q2);
realColor.g = Math.round(g * q1 + realColor.g * q2);
realColor.b = Math.round(b * q1 + realColor.b * q2);
realColor.a = mixedA;
}
// https://www.w3.org/TR/AERT#color-contrast
const {r, g, b} = realColor;
const brightness = r * .299 + g * .587 + b * .114;
return brightness < 128 ? 'dark' : 'light';
}
function constrain(min, max, value) {
return value < min ? min : value > max ? max : value;
}
function snapToInt(num) {
const int = Math.round(num);
return Math.abs(int - num) < 1e-3 ? int : num;
}
function parseAs(el, parser) {
const num = parser(el.value);
if (!isNaN(num) &&
(!el.min || num >= parseFloat(el.min)) &&
(!el.max || num <= parseFloat(el.max))) {
el.value = num;
return true;
}
}
//endregion
};

View File

@ -0,0 +1,508 @@
/* global CodeMirror */
'use strict';
(() => {
const OWN_TOKEN_NAME = 'colorview';
const OWN_DOM_CLASS = 'cm-' + OWN_TOKEN_NAME;
const OWN_BACKGROUND_CLASS = 'codemirror-colorview-background';
const HOOKED_TOKEN = new Map([
['atom', colorizeAtom],
['keyword', colorizeKeyword],
].map(([name, fn]) => [name, {override: name + ' ' + OWN_TOKEN_NAME, process: fn}]));
const NAMED_COLORS = getNamedColorsMap();
const TRANSPARENT = {
color: 'transparent',
colorValue: 'rgba(0, 0, 0, 0)', // as per the CSS spec
};
const RX_COLOR = {
hex: /#(?:[a-f\d]{3,4}|[a-f\d]{6}|[a-f\d]{8})\b/yi,
rgb: /rgb\((?:\s*\d{1,3}\s*,\s*){2}\d{1,3}\s*\)/yi,
rgba: /rgba\((?:\s*\d{1,3}\s*,\s*){3}\d*\.?\d+\s*\)/yi,
hsl: /hsl\(\s*(?:-?\d+|-?\d*\.\d+)\s*(?:,\s*(?:-?\d+|-?\d*\.\d+)%\s*){2}\)/yi,
hsla: /hsla\(\s*(?:-?\d+|-?\d*\.\d+)\s*(?:,\s*(?:-?\d+|-?\d*\.\d+)%\s*){2},\s*(?:-?\d+|-?\d*\.\d+)\s*\)/yi,
};
const CodeMirrorEvents = {
update(cm) {
if (cm.state.colorpicker.cache.size) {
renderVisibleTokens(cm);
}
},
keyup(cm) {
const popup = cm.state.colorpicker.popup;
if (popup && popup.options.isShortCut === false) {
popup.hide();
}
},
mousedown(cm, event) {
const self = cm.state.colorpicker;
const isMarker = event.button === 0 && event.target.classList.contains(OWN_BACKGROUND_CLASS);
window.dispatchEvent(new CustomEvent('close-colorpicker-popup', {detail: isMarker && self.popup}));
if (isMarker) {
event.preventDefault();
self.openPopupForToken(event.target.parentNode);
}
},
};
function registerEvents(cm) {
Object.keys(CodeMirrorEvents).forEach(name => cm.on(name, CodeMirrorEvents[name]));
}
function unregisterEvents(cm) {
Object.keys(CodeMirrorEvents).forEach(name => cm.off(name, CodeMirrorEvents[name]));
}
function registerHooks() {
const mx = CodeMirror.modeExtensions.css;
if (!mx || mx.token !== colorizeToken) {
CodeMirror.extendMode('css', {token: colorizeToken});
CodeMirror.extendMode('stylus', {token: colorizeToken});
}
}
function unregisterHooks() {
for (const name in CodeMirror.modeExtensions) {
const mx = CodeMirror.modeExtensions[name];
if (mx && mx.token === colorizeToken) {
delete mx.token;
}
}
}
function resetMode(cm) {
cm.setOption('mode', cm.getMode().name);
}
function colorizeToken(stream, state) {
const token = this._token.apply(this, arguments);
const hookedToken = token && HOOKED_TOKEN.get(token);
if (!token || !hookedToken) {
return token;
}
const data = state.colorpicker = (state.colorpicker || {});
const cache = data.cache = (data.cache || stream.lineOracle.doc.cm.state.colorpicker.cache);
const string = stream.string;
const sameString = string === data.lastString;
data.lastString = string;
let lineCache = data.lineCache = (sameString ? data.lineCache : cache.get(string));
if (lineCache && lineCache.get(stream.start)) {
return hookedToken.override;
}
const color = hookedToken.process(stream);
if (color) {
if (!lineCache) {
lineCache = data.lineCache = new Map();
cache.set(string, lineCache);
}
lineCache.set(stream.start, color);
lineCache.set('lastAccessTime', performance.now());
return hookedToken.override;
}
return token;
}
function colorizeAtom(stream) {
const {start, pos, string} = stream;
const c1 = string.charAt(start);
if ((c1 === 't' || c1 === 'T') && string.slice(start, pos).toLowerCase() === 'transparent') {
return TRANSPARENT;
}
const maybeHex = c1 === '#';
const s = !maybeHex && string.charAt(pos) === '(' && string.slice(start, pos).toLowerCase();
if (maybeHex || (s === 'rgb' || s === 'rgba' || s === 'hsl' || s === 'hsla')) {
const rx = maybeHex ? RX_COLOR.hex : RX_COLOR[s];
rx.lastIndex = start;
const match = rx.exec(string);
return match && {color: match[0]};
}
}
function colorizeKeyword(stream) {
const {start, pos, string} = stream;
if (string.charAt(start) !== '!') {
const color = string.slice(start, pos);
const colorValue = NAMED_COLORS.get(color.toLowerCase());
return colorValue ? {color, colorValue} : colorizeAtom(stream);
}
}
function renderVisibleTokens(cm) {
const {cache, options} = cm.state.colorpicker;
let line = cm.display.viewFrom - 1;
for (const {line: lineHandle, text} of cm.display.renderedView) {
if (!lineHandle.parent) {
continue;
}
line++;
const styles = lineHandle.styles;
if (!styles) {
continue;
}
const lineCache = cache.get(lineHandle.text);
if (!lineCache) {
continue;
}
let lineCacheAlive = false;
let elementIndex = 0;
let elements;
for (let i = 1; i < styles.length; i += 2) {
const token = styles[i + 1];
if (!token || !token.includes(OWN_TOKEN_NAME)) {
continue;
}
const start = styles[i - 2] || 0;
const data = lineCache.get(start);
if (!data) {
continue;
}
elements = elements || text.getElementsByClassName(OWN_DOM_CLASS);
const el = elements[elementIndex++];
while (true) {
const nextStyle = styles[i + 3];
const nextStart = styles[i];
if (nextStyle && nextStyle.includes(OWN_TOKEN_NAME) &&
nextStart > start && nextStart <= start + data.color.length) {
elementIndex++;
i += 2;
} else {
break;
}
}
if (el.colorpickerData && el.colorpickerData.color === data.color) {
continue;
}
el.colorpickerData = Object.assign({line, ch: start}, data);
let bg = el.firstElementChild;
if (!bg) {
bg = document.createElement('div');
bg.className = OWN_BACKGROUND_CLASS;
bg.title = options.tooltip;
el.appendChild(bg);
}
bg.style.setProperty('background-color', data.color, 'important');
lineCacheAlive = true;
}
if (lineCacheAlive) {
lineCache.set('lastAccessTime', performance.now());
}
}
trimCache(cm);
}
function trimCache(cm, debounced) {
if (!debounced) {
clearTimeout(trimCache.timer);
trimCache.timer = setTimeout(trimCache, 20e3, cm, true);
return;
}
const cutoff = performance.now() - 60e3;
const {cache} = cm.state.colorpicker;
const textToKeep = new Set();
cm.doc.iter(({text}) => textToKeep.add(text));
for (const [text, lineCache] of cache.entries()) {
if (lineCache.get('lastAccessTime') < cutoff && !textToKeep.has(text)) {
cache.delete(text);
}
}
}
function parseColorAtCursor(lineText, lineTextLC = lineText.toLowerCase(), ch) {
const iHex = lineTextLC.lastIndexOf('#', ch);
const iParen = (
lineTextLC.lastIndexOf('(', ch) + 1 ||
lineTextLC.indexOf('(', ch) + 1
) - 1;
let start = Math.max(iHex, iParen);
let match, end, color, colorValue;
if (start >= 0) {
if (start === iHex) {
match = RX_COLOR.hex;
} else {
const tokenLen = lineTextLC.charAt(start - 1) === 'a' ? 4 : 3;
start -= tokenLen;
match = RX_COLOR[lineTextLC.substr(start, tokenLen)];
}
if (match) {
match.lastIndex = start;
([color] = match.exec(lineText) || []);
}
} else {
const isLetterAt = (i, code = lineTextLC.charCodeAt(i)) => code >= 97 && code <= 122;
for (start = ch; isLetterAt(start); start--) {} // eslint-disable-line no-empty
for (end = ch; isLetterAt(end); end++) {} // eslint-disable-line no-empty
start++;
(color = lineTextLC.slice(start, end));
colorValue = NAMED_COLORS.get(color);
if (!colorValue) {
start = ch;
color = '';
}
}
return color && {ch: start, color, colorValue};
}
function getNamedColorsMap() {
return new Map([
['aliceblue', '#f0f8ff'],
['antiquewhite', '#faebd7'],
['aqua', '#00ffff'],
['aquamarine', '#7fffd4'],
['azure', '#f0ffff'],
['beige', '#f5f5dc'],
['bisque', '#ffe4c4'],
['black', '#000000'],
['blanchedalmond', '#ffebcd'],
['blue', '#0000ff'],
['blueviolet', '#8a2be2'],
['brown', '#a52a2a'],
['burlywood', '#deb887'],
['cadetblue', '#5f9ea0'],
['chartreuse', '#7fff00'],
['chocolate', '#d2691e'],
['coral', '#ff7f50'],
['cornflowerblue', '#6495ed'],
['cornsilk', '#fff8dc'],
['crimson', '#dc143c'],
['cyan', '#00ffff'],
['darkblue', '#00008b'],
['darkcyan', '#008b8b'],
['darkgoldenrod', '#b8860b'],
['darkgray', '#a9a9a9'],
['darkgreen', '#006400'],
['darkgrey', '#a9a9a9'],
['darkkhaki', '#bdb76b'],
['darkmagenta', '#8b008b'],
['darkolivegreen', '#556b2f'],
['darkorange', '#ff8c00'],
['darkorchid', '#9932cc'],
['darkred', '#8b0000'],
['darksalmon', '#e9967a'],
['darkseagreen', '#8fbc8f'],
['darkslateblue', '#483d8b'],
['darkslategray', '#2f4f4f'],
['darkslategrey', '#2f4f4f'],
['darkturquoise', '#00ced1'],
['darkviolet', '#9400d3'],
['deeppink', '#ff1493'],
['deepskyblue', '#00bfff'],
['dimgray', '#696969'],
['dimgrey', '#696969'],
['dodgerblue', '#1e90ff'],
['firebrick', '#b22222'],
['floralwhite', '#fffaf0'],
['forestgreen', '#228b22'],
['fuchsia', '#ff00ff'],
['gainsboro', '#dcdcdc'],
['ghostwhite', '#f8f8ff'],
['gold', '#ffd700'],
['goldenrod', '#daa520'],
['gray', '#808080'],
['green', '#008000'],
['greenyellow', '#adff2f'],
['grey', '#808080'],
['honeydew', '#f0fff0'],
['hotpink', '#ff69b4'],
['indianred', '#cd5c5c'],
['indigo', '#4b0082'],
['ivory', '#fffff0'],
['khaki', '#f0e68c'],
['lavender', '#e6e6fa'],
['lavenderblush', '#fff0f5'],
['lawngreen', '#7cfc00'],
['lemonchiffon', '#fffacd'],
['lightblue', '#add8e6'],
['lightcoral', '#f08080'],
['lightcyan', '#e0ffff'],
['lightgoldenrodyellow', '#fafad2'],
['lightgray', '#d3d3d3'],
['lightgreen', '#90ee90'],
['lightgrey', '#d3d3d3'],
['lightpink', '#ffb6c1'],
['lightsalmon', '#ffa07a'],
['lightseagreen', '#20b2aa'],
['lightskyblue', '#87cefa'],
['lightslategray', '#778899'],
['lightslategrey', '#778899'],
['lightsteelblue', '#b0c4de'],
['lightyellow', '#ffffe0'],
['lime', '#00ff00'],
['limegreen', '#32cd32'],
['linen', '#faf0e6'],
['magenta', '#ff00ff'],
['maroon', '#800000'],
['mediumaquamarine', '#66cdaa'],
['mediumblue', '#0000cd'],
['mediumorchid', '#ba55d3'],
['mediumpurple', '#9370db'],
['mediumseagreen', '#3cb371'],
['mediumslateblue', '#7b68ee'],
['mediumspringgreen', '#00fa9a'],
['mediumturquoise', '#48d1cc'],
['mediumvioletred', '#c71585'],
['midnightblue', '#191970'],
['mintcream', '#f5fffa'],
['mistyrose', '#ffe4e1'],
['moccasin', '#ffe4b5'],
['navajowhite', '#ffdead'],
['navy', '#000080'],
['oldlace', '#fdf5e6'],
['olive', '#808000'],
['olivedrab', '#6b8e23'],
['orange', '#ffa500'],
['orangered', '#ff4500'],
['orchid', '#da70d6'],
['palegoldenrod', '#eee8aa'],
['palegreen', '#98fb98'],
['paleturquoise', '#afeeee'],
['palevioletred', '#db7093'],
['papayawhip', '#ffefd5'],
['peachpuff', '#ffdab9'],
['peru', '#cd853f'],
['pink', '#ffc0cb'],
['plum', '#dda0dd'],
['powderblue', '#b0e0e6'],
['purple', '#800080'],
['rebeccapurple', '#663399'],
['red', '#ff0000'],
['rosybrown', '#bc8f8f'],
['royalblue', '#4169e1'],
['saddlebrown', '#8b4513'],
['salmon', '#fa8072'],
['sandybrown', '#f4a460'],
['seagreen', '#2e8b57'],
['seashell', '#fff5ee'],
['sienna', '#a0522d'],
['silver', '#c0c0c0'],
['skyblue', '#87ceeb'],
['slateblue', '#6a5acd'],
['slategray', '#708090'],
['slategrey', '#708090'],
['snow', '#fffafa'],
['springgreen', '#00ff7f'],
['steelblue', '#4682b4'],
['tan', '#d2b48c'],
['teal', '#008080'],
['thistle', '#d8bfd8'],
['tomato', '#ff6347'],
['turquoise', '#40e0d0'],
['violet', '#ee82ee'],
['wheat', '#f5deb3'],
['white', '#ffffff'],
['whitesmoke', '#f5f5f5'],
['yellow', '#ffff00'],
['yellowgreen', '#9acd32'],
]);
}
class ColorMarker {
constructor(cm, {
tooltip = 'Open color picker',
popupOptions = {},
colorpicker,
forceUpdate,
} = {}) {
this.cm = cm;
this.options = {
tooltip,
popup: Object.assign({
hideDelay: 2000,
hexUppercase: false,
tooltipForSwitcher: 'Switch formats: HEX -> RGB -> HSL',
}, popupOptions),
};
this.popup = cm.colorpicker ? cm.colorpicker() : colorpicker;
this.cache = new Map();
registerHooks(cm);
registerEvents(cm);
if (forceUpdate) {
resetMode(cm);
}
}
destroy() {
unregisterHooks(this.cm);
unregisterEvents(this.cm);
resetMode(this.cm);
this.cm.state.colorpicker = null;
}
openPopup(color) {
let {line, ch} = this.cm.getCursor();
const lineText = this.cm.getLine(line);
const lineTextLC = lineText.toLowerCase();
const atImportant = lineTextLC.lastIndexOf('!important', ch);
if (atImportant >= Math.max(0, ch - '!important'.length)) {
ch -= Math.max(0, ch - atImportant);
}
const data = {line, ch, colorValue: color, isShortCut: true};
const lineCache = this.cm.state.colorpicker.cache.get(lineText);
if (lineCache) {
for (const [start, {color, colorValue = color}] of lineCache.entries()) {
// one entry is for lastAccessTime
if (lineCache.size === 2 || start <= ch && ch <= start + color.length) {
Object.assign(data, {ch: start, color, colorValue});
this.openPopupForToken({colorpickerData: data});
return;
}
}
}
Object.assign(data, parseColorAtCursor(lineText, lineTextLC, ch));
this.openPopupForToken({colorpickerData: data});
}
openPopupForToken({colorpickerData: data}) {
if (this.popup) {
const {left, bottom: top} = this.cm.charCoords(data, 'window');
this.popup.show(Object.assign(this.options.popup, data, {
top,
left,
cm: this.cm,
color: data.colorValue || data.color,
prevColor: data.color || '',
isShortCut: false,
callback: ColorMarker.popupOnChange,
}));
}
}
closePopup() {
if (this.popup) {
this.popup.hide();
}
}
static popupOnChange(newColor) {
if (!newColor) {
return;
}
const {cm, line, ch, embedderCallback} = this;
const to = {line, ch: ch + this.prevColor.length};
if (cm.getRange(this, to) !== newColor) {
cm.replaceRange(newColor, this, to, '*colorpicker');
this.prevColor = newColor;
}
if (typeof embedderCallback === 'function') {
embedderCallback(this);
}
}
}
CodeMirror.defineOption('colorpicker', false, (cm, value, oldValue) => {
if (oldValue && oldValue !== CodeMirror.Init && cm.state.colorpicker) {
cm.state.colorpicker.destroy();
}
if (value) {
cm.state.colorpicker = new ColorMarker(cm, value);
}
});
// initial runMode is performed by CodeMirror before setting our option
// so we register the hooks right away - not a problem as our js is loaded on demand
registerHooks();
})();

View File

@ -3343,9 +3343,9 @@ var Properties = module.exports = {
__proto__: null,
//A
"align-items" : "flex-start | flex-end | center | baseline | stretch",
"align-content" : "flex-start | flex-end | center | space-between | space-around | stretch",
"align-self" : "auto | flex-start | flex-end | center | baseline | stretch",
"align-items" : "normal | stretch | <baseline-position> | [ <overflow-position>? <self-position> ]",
"align-content" : "<align-content>",
"align-self" : "<align-self>",
"all" : "initial | inherit | unset",
"-webkit-align-items" : "flex-start | flex-end | center | baseline | stretch",
"-webkit-align-content" : "flex-start | flex-end | center | space-between | space-around | stretch",
@ -3404,7 +3404,7 @@ var Properties = module.exports = {
"backface-visibility" : "visible | hidden",
"background" : 1,
"background-attachment" : "<attachment>#",
"background-blend-mode" : "normal | multiply | screen | overlay | darken | lighten | color-dodge | color-burn | hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosity",
"background-blend-mode" : "<blend-mode>",
"background-clip" : "<box>#",
"background-color" : "<color>",
"background-image" : "<bg-image>#",
@ -3494,7 +3494,7 @@ var Properties = module.exports = {
"color-rendering" : "auto | optimizeSpeed | optimizeQuality",
"column-count" : "<integer> | auto", //https://www.w3.org/TR/css3-multicol/
"column-fill" : "auto | balance",
"column-gap" : "<length> | normal",
"column-gap" : "normal | <length> | <percentage>",
"column-rule" : "<border-width> || <border-style> || <color>",
"column-rule-color" : "<color>",
"column-rule-style" : "<border-style>",
@ -3576,6 +3576,7 @@ var Properties = module.exports = {
"font-weight" : "<font-weight>",
//G
"gap" : "[ <length> | normal ]{1,2}",
"glyph-orientation-horizontal" : "<glyph-angle>",
"glyph-orientation-vertical" : "auto | <glyph-angle>",
"grid" : 1,
@ -3623,9 +3624,12 @@ var Properties = module.exports = {
"image-resolution" : 1,
"ime-mode" : "auto | normal | active | inactive | disabled",
"inline-box-align" : "last | <integer>",
"isolation" : "auto | isolate",
//J
"justify-content" : "flex-start | flex-end | center | space-between | space-around",
"justify-content" : "<justify-content>",
"justify-items" : "normal | stretch | <baseline-position> | [ <overflow-position>? <self-position> ] | [ legacy || [ left | right | center ] ]",
"justify-self" : "<justify-self>",
"-webkit-justify-content" : "flex-start | flex-end | center | space-between | space-around",
//K
@ -3668,6 +3672,7 @@ var Properties = module.exports = {
"max-width" : "<length> | <percentage> | <content-sizing> | none",
"min-height" : "<length> | <percentage> | <content-sizing> | contain-floats | -moz-contain-floats | -webkit-contain-floats",
"min-width" : "<length> | <percentage> | <content-sizing> | contain-floats | -moz-contain-floats | -webkit-contain-floats",
"mix-blend-mode" : "<blend-mode>",
"move-to" : 1,
//N
@ -3714,6 +3719,9 @@ var Properties = module.exports = {
"phonemes" : 1,
"pitch" : 1,
"pitch-range" : 1,
"place-content" : "<align-content> <justify-content>?",
"place-items" : "[ normal | stretch | <baseline-position> | <self-position> ] [ normal | stretch | <baseline-position> | <self-position> ]?",
"place-self" : "<align-self> <justify-self>?",
"play-during" : 1,
"pointer-events" : "auto | none | visiblePainted | visibleFill | visibleStroke | visible | painted | fill | stroke | all",
"position" : "static | relative | absolute | fixed | sticky | -webkit-sticky",
@ -3733,6 +3741,7 @@ var Properties = module.exports = {
"right" : "<margin-width>",
"rotation" : 1,
"rotation-point" : 1,
"row-gap" : "normal | <length> | <percentage>",
"ruby-align" : 1,
"ruby-overhang" : 1,
"ruby-position" : 1,
@ -5660,7 +5669,7 @@ var Tokens = module.exports = [
// ignorables
{ name: "S", whitespace: true/*, channel: "ws"*/ },
{ name: "COMMENT", comment: true, hide: true, channel: "comment" },
{ name: "COMMENT", whitespace: true, comment: true, hide: true/*, channel: "comment"*/ },
// attribute equality
{ name: "INCLUDES", text: "~=" },
@ -6013,12 +6022,19 @@ copy(ValidationTypes, {
},
describe: function(type) {
if (this.complex[type] instanceof Matcher) {
return this.complex[type].toString(0);
}
return type;
const complex = this.complex[type];
const text = complex instanceof Matcher ? complex.toString(0) : type;
return this.explode(text);
},
explode(text) {
return !text.includes('<') ? text :
text.replace(/(<.*?>)([{#?])?/g, (_, rule, mod) => {
const ref = this.simple[rule] || this.complex[rule];
return !ref || !ref.originalText ? rule :
(mod ? '[' : '') + this.explode(ref.originalText) + (mod ? ']' : '');
});
},
/**
* Determines if the next part(s) of the given expression
* are any of the given types.
@ -6107,6 +6123,8 @@ copy(ValidationTypes, {
"<bg-image>": "<image> | <gradient> | none",
"<blend-mode>": "normal | multiply | screen | overlay | darken | lighten | color-dodge | color-burn | hard-light | soft-light | difference | exclusion | hue | saturation | color | luminosity",
"<border-style>":
"none | hidden | dotted | dashed | solid | double | groove | " +
"ridge | inset | outset",
@ -6128,6 +6146,9 @@ copy(ValidationTypes, {
"<content>": "content()",
"<content-distribution>": "space-between | space-around | space-evenly | stretch",
"<content-position>": "center | start | end | flex-start | flex-end",
// https://www.w3.org/TR/css3-sizing/#width-height-keywords
"<content-sizing>":
"fill-available | -moz-available | -webkit-fill-available | " +
@ -6245,6 +6266,8 @@ copy(ValidationTypes, {
return this["<number>"](part) && part.value >= 0 && part.value <= 1;
},
"<overflow-position>": "unsafe | safe",
"<padding-width>": "<nonnegative-length-or-percentage>",
"<percentage>": function(part) {
@ -6253,6 +6276,8 @@ copy(ValidationTypes, {
"<relative-size>": "smaller | larger",
"<self-position>": "center | start | end | self-start | self-end | flex-start | flex-end",
"<shape>": "rect() | inset-rect()",
"<shape-box>": "<box> | margin-box",
@ -6290,6 +6315,12 @@ copy(ValidationTypes, {
complex: {
__proto__: null,
"<align-content>":
"normal | <baseline-position> | <content-distribution> | <overflow-position>? <content-position>",
"<align-self>":
"auto | normal | stretch | <baseline-position> | <overflow-position>? <self-position>",
"<azimuth>":
"<angle>" +
" | " +
@ -6298,6 +6329,8 @@ copy(ValidationTypes, {
" | "+
"leftwards | rightwards",
"<baseline-position>": "[ first | last ]? baseline",
"<bg-position>": "<position>#",
"<bg-size>":
@ -6385,6 +6418,12 @@ copy(ValidationTypes, {
"[ full-width | proportional-width ] || " +
"ruby",
"<justify-content>":
"normal | <content-distribution> | <overflow-position>? [ <content-position> | left | right ]",
"<justify-self>":
"auto | normal | stretch | <baseline-position> | <overflow-position>? [ <self-position> | left | right ]",
// Note that <color> here is "as defined in the SVG spec", which
// is more restrictive that the <color> defined in the CSS spec.
// none | currentColor | <color> [<icccolor>]? |
@ -6392,7 +6431,8 @@ copy(ValidationTypes, {
"<paint>": "<paint-basic> | <uri> <paint-basic>?",
// Helper definition for <paint> above.
"<paint-basic>": "none | currentColor | <color-svg> <icccolor>?",
// Note: "transparent" is an accepted color now in SVG
"<paint-basic>": "none | currentColor | <color-svg> <icccolor>? | transparent",
"<position>":
// Because our `alt` combinator is ordered, we need to test these
@ -6434,16 +6474,16 @@ copy(ValidationTypes, {
Object.keys(ValidationTypes.simple).forEach(function(nt) {
var rule = ValidationTypes.simple[nt];
if (typeof rule === "string") {
ValidationTypes.simple[nt] = function(part) {
ValidationTypes.simple[nt] = Object.defineProperty(function(part) {
return ValidationTypes.isLiteral(part, rule);
};
}, 'originalText', {value: rule});
}
});
Object.keys(ValidationTypes.complex).forEach(function(nt) {
var rule = ValidationTypes.complex[nt];
if (typeof rule === "string") {
ValidationTypes.complex[nt] = Matcher.parse(rule);
ValidationTypes.complex[nt] = Object.defineProperty(Matcher.parse(rule), 'originalText', {value: rule});
}
});
@ -7103,15 +7143,12 @@ TokenStreamBase.prototype = {
tokenTypes = [tokenTypes];
}
var tt = this.get(channel),
i = 0,
len = tokenTypes.length;
while (i < len) {
if (tt === tokenTypes[i++]) {
do {
var tt = this.get(channel);
if (tokenTypes.includes(tt)) {
return true;
}
}
} while (tt === 4 && this.LA(0) !== 0);
//no match found, put the token back
this.unget();
@ -7136,7 +7173,7 @@ TokenStreamBase.prototype = {
tokenTypes = [tokenTypes];
}
if (!this.match.apply(this, arguments)) {
if (!this.match(tokenTypes)) {
token = this.LT(1);
throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name +
" at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
@ -10930,31 +10967,48 @@ CSSLint.addFormatter({
}
});
/*
* Web worker for CSSLint
*/
if (!CSSLint.suppressUsoVarError) {
CSSLint.suppressUsoVarError = true;
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
const isUsoVar = ({value}) => value.startsWith('/*[[') && value.endsWith(']]*/');
CSSLint.addRule({
id: 'uso-vars',
init(parser, reporter) {
parser.addListener('error', function ({message, line, col}) {
if (!isUsoVar(this._tokenStream._token)) {
const {_lt, _ltIndex: i} = this._tokenStream;
if (i < 2 || !_lt.slice(0, i - 1).reverse().some(isUsoVar)) {
reporter.error(message, line, col);
}
}
});
},
});
}
/* global self, JSON */
self.onmessage = ({data: {action = 'run', code, config}}) => {
switch (action) {
// message indicates to start linting
self.onmessage = function(event) {
"use strict";
var data = event.data,
message,
text,
ruleset,
results;
case 'getAllRuleIds':
// the functions are non-tranferable and we need only an id
self.postMessage(CSSLint.getRules().map(rule => rule.id));
return;
try {
message = JSON.parse(data);
text = message.text;
ruleset = message.ruleset;
} catch (ex) {
text = data;
}
case 'run':
Object.defineProperty(config, 'errors', {get: () => 0, set: () => 0});
config['uso-vars'] = 1;
self.postMessage(CSSLint.verify(code, config).messages.map(m => {
// the functions are non-tranferable and we need only an id
m.rule = {id: m.rule.id};
return m;
}));
return;
results = CSSLint.verify(text, ruleset);
// Not all browsers support structured clone, so JSON stringify results
self.postMessage(JSON.stringify(results));
case 'parse':
if (!self.mozParser) {
self.importScripts('/js/moz-parser.js');
}
mozParser.parse(code)
.then(sections => self.postMessage(sections));
}
};

View File

@ -1672,4 +1672,60 @@ N,R,y-N,"inline"])):(K.lastIndex=G+1,K.test(E),y=0===K.lastIndex?E.length-1:K.la
{}],609:[function(a,l,g){l.exports=function(a,g,f){if(0===a.length)return a;if(g){f||a.sort(g);f=1;for(var d=a.length,c=a[0],b,h=1;h<d;++h)b=c,c=a[h],g(c,b)&&(h===f?f++:a[f++]=c);a.length=f;return a}f||a.sort();g=1;f=a.length;d=a[0];for(b=1;b<f;++b)c=d,d=a[b],d!==c&&(b===g?g++:a[g++]=d);a.length=g;return a}},{}],610:[function(a,l,g){function k(a,f){function d(){for(var c=Array(arguments.length),b=0;b<c.length;b++)c[b]=arguments[b];var d=a.apply(this,c),f=c[c.length-1];"function"===typeof d&&d!==f&&
Object.keys(f).forEach(function(a){d[a]=f[a]});return d}if(a&&f)return k(a)(f);if("function"!==typeof a)throw new TypeError("need wrapper function");Object.keys(a).forEach(function(c){d[c]=a[c]});return d}l.exports=k},{}],611:[function(a,l,g){var k=a("fs"),h=a("path"),f=a("mkdirp");l.exports=function(a,c,b){var d=h.dirname(a);k.exists(d,function(g){g?k.writeFile(a,c,b):f(d,function(d){if(d)return b(d);k.writeFile(a,c,b)})})};l.exports.sync=function(a,c){var b=h.dirname(a);k.existsSync(b)||f.sync(b);
k.writeFileSync(a,c)};l.exports.stream=function(a){var c=h.dirname(a);k.existsSync(c)||f.sync(c);return k.createWriteStream(a)}},{fs:1,mkdirp:173,path:14}],stylelint:[function(a,l,g){g=a("./utils/checkAgainstRule");var k=a("./createPlugin"),h=a("./createStylelint"),f=a("./formatters"),d=a("./postcssPlugin"),c=a("./utils/report"),b=a("./utils/ruleMessages"),p=a("./rules"),m=a("./standalone");a=a("./utils/validateOptions");d.utils={report:c,ruleMessages:b,validateOptions:a,checkAgainstRule:g};d.lint=
m;d.rules=p;d.formatters=f;d.createPlugin=k;d.createLinter=h;l.exports=d},{"./createPlugin":334,"./createStylelint":335,"./formatters":338,"./postcssPlugin":346,"./rules":429,"./standalone":520,"./utils/checkAgainstRule":529,"./utils/report":592,"./utils/ruleMessages":593,"./utils/validateOptions":595}]},{},[]);
m;d.rules=p;d.formatters=f;d.createPlugin=k;d.createLinter=h;l.exports=d},{"./createPlugin":334,"./createStylelint":335,"./formatters":338,"./postcssPlugin":346,"./rules":429,"./standalone":520,"./utils/checkAgainstRule":529,"./utils/report":592,"./utils/ruleMessages":593,"./utils/validateOptions":595}]},{},[]);
(() => {
const stylelint = require('stylelint');
self.onmessage = ({data: {action = 'run', code, config}}) => {
switch (action) {
case 'getAllRuleIds':
// the functions are non-tranferable
self.postMessage(Object.keys(stylelint.rules));
return;
case 'getAllRuleOptions':
self.postMessage(getAllRuleOptions());
return;
case 'run':
stylelint.lint({code, config}).then(results =>
self.postMessage(results));
return;
}
};
function getAllRuleOptions() {
const options = {};
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
const rxString = /"([-\w\s]{3,}?)"/g;
for (const id of Object.keys(stylelint.rules)) {
const ruleCode = String(stylelint.rules[id]);
const sets = [];
let m, mStr;
while ((m = rxPossible.exec(ruleCode))) {
const possible = m[1];
const set = [];
while ((mStr = rxString.exec(possible))) {
const s = mStr[1];
if (s.includes(' ')) {
set.push(...s.split(/\s+/));
} else {
set.push(s);
}
}
if (possible.includes('ignoreAtRules')) {
set.push('ignoreAtRules');
}
if (possible.includes('ignoreShorthands')) {
set.push('ignoreShorthands');
}
if (set.length) {
sets.push(set);
}
}
if (sets.length) {
options[id] = sets;
}
}
return options;
}
})();

View File

@ -172,10 +172,6 @@
if (open == -1) return false
var endLine = end == start ? startLine : self.getLine(end)
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
if (close == -1 && start != end) {
endLine = self.getLine(--end);
close = endLine.indexOf(endString);
}
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
if (close == -1 ||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||

View File

@ -133,7 +133,8 @@
(cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) {
curType = "addFour";
} else if (identical) {
if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both";
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
else return CodeMirror.Pass;
} else if (opening && (cm.getLine(cur.line).length == cur.ch ||
isClosingBracket(next, pairs) ||
@ -185,22 +186,6 @@
return str.length == 2 ? str : null;
}
// Project the token type that will exists after the given char is
// typed, and use it to determine whether it would cause the start
// of a string token.
function enteringString(cm, pos, ch) {
var line = cm.getLine(pos.line);
var token = cm.getTokenAt(pos);
if (/\bstring2?\b/.test(token.type) || stringStartsAfter(cm, pos)) return false;
var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
stream.pos = stream.start = token.start;
for (;;) {
var type1 = cm.getMode().token(stream, token.state);
if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
stream.start = stream.pos;
}
}
function stringStartsAfter(cm, pos) {
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
return /\bstring/.test(token.type) && token.start == pos.ch &&

View File

@ -121,7 +121,6 @@
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close();
if (data && this.data && isNewCompletion(this.data, data)) return;
this.data = data;
if (data && data.list.length) {
@ -135,11 +134,6 @@
}
};
function isNewCompletion(old, nw) {
var moved = CodeMirror.cmpPos(nw.from, old.from)
return moved > 0 && old.to.ch - old.from.ch != nw.to.ch - nw.from.ch
}
function parseOptions(cm, pos, options) {
var editor = cm.options.hintOptions;
var out = {};

View File

@ -1,64 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), "cjs");
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], function(CM) { mod(CM, "amd"); });
else // Plain browser env
mod(CodeMirror, "plain");
})(function(CodeMirror, env) {
if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js";
var loading = {};
function splitCallback(cont, n) {
var countDown = n;
return function() { if (--countDown == 0) cont(); };
}
function ensureDeps(mode, cont) {
var deps = CodeMirror.modes[mode].dependencies;
if (!deps) return cont();
var missing = [];
for (var i = 0; i < deps.length; ++i) {
if (!CodeMirror.modes.hasOwnProperty(deps[i]))
missing.push(deps[i]);
}
if (!missing.length) return cont();
var split = splitCallback(cont, missing.length);
for (var i = 0; i < missing.length; ++i)
CodeMirror.requireMode(missing[i], split);
}
CodeMirror.requireMode = function(mode, cont) {
if (typeof mode != "string") mode = mode.name;
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont);
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont);
var file = CodeMirror.modeURL.replace(/%N/g, mode);
if (env == "plain") {
var script = document.createElement("script");
script.src = file;
var others = document.getElementsByTagName("script")[0];
var list = loading[mode] = [cont];
CodeMirror.on(script, "load", function() {
ensureDeps(mode, function() {
for (var i = 0; i < list.length; ++i) list[i]();
});
});
others.parentNode.insertBefore(script, others);
} else if (env == "cjs") {
require(file);
cont();
} else if (env == "amd") {
requirejs([file], cont);
}
};
CodeMirror.autoLoadMode = function(instance, mode) {
if (!CodeMirror.modes.hasOwnProperty(mode))
CodeMirror.requireMode(mode, function() {
instance.setOption("mode", instance.getOption("mode"));
});
};
});

View File

@ -19,12 +19,6 @@
// highlighting the matches. If annotateScrollbar is enabled, the occurences
// will be highlighted on the scrollbar via the matchesonscrollbar addon.
/* STYLUS: hack start (part 1) */
/* eslint curly: 1, brace-style:1, strict: 0, quotes: 0, semi: 1, indent: 1 */
/* eslint no-var: 0, block-scoped-var: 0, no-redeclare: 0, no-unused-expressions: 1 */
/* global CodeMirror, require, define */
/* STYLUS: hack end (part 1) */
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
@ -94,9 +88,7 @@
function addOverlay(cm, query, hasBoundary, style) {
var state = cm.state.matchHighlighter;
/* STYLUS: hack start (part 2) */
cm.addOverlay(state.overlay = makeOverlay(cm, query, hasBoundary, style));
/* STYLUS: hack end (part 2) */
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
@ -119,24 +111,16 @@
function highlightMatches(cm) {
cm.operation(function() {
var state = cm.state.matchHighlighter;
removeOverlay(cm);
if (!cm.somethingSelected() && state.options.showToken) {
var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
while (start && re.test(line.charAt(start - 1))) --start;
while (end < line.length && re.test(line.charAt(end))) ++end;
/* STYLUS: hack start */
const token = line.slice(start, end);
if (token !== state.lastToken) {
state.lastToken = token;
removeOverlay(cm);
if (token) {
addOverlay(cm, token, re, state.options.style);
}
}
if (start < end)
addOverlay(cm, line.slice(start, end), re, state.options.style);
return;
}
removeOverlay(cm);
/* STYLUS: hack end */
var from = cm.getCursor("from"), to = cm.getCursor("to");
if (from.line != to.line) return;
if (state.options.wordsOnly && !isWord(cm, from, to)) return;
@ -169,28 +153,11 @@
(stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
}
function makeOverlay(cm, query, hasBoundary, style) {
/* STYLUS: hack start (part 3) */
const approvedClassName = `cm-${style}-approved`;
let timer;
let occurrences = 0;
function makeOverlay(query, hasBoundary, style) {
return {token: function(stream) {
clearTimeout(timer);
timer = setTimeout(() => {
occurrences = 0;
timer = null;
});
if (stream.match(query) &&
(!hasBoundary || boundariesAround(stream, hasBoundary))) {
occurrences++;
if (occurrences == 1) {
cm.display.wrapper.classList.remove(approvedClassName);
} else if (occurrences == 2) {
cm.display.wrapper.classList.add(approvedClassName);
}
(!hasBoundary || boundariesAround(stream, hasBoundary)))
return style;
}
/* STYLUS: hack end (part 3) */
stream.next();
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
}};

View File

@ -159,7 +159,7 @@
for (var i = 1; i < lines.length - 1; i++)
if (fold(doc.getLine(line + i)) != lines[i]) continue search
var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]
if (end.slice(0, lastLine.length) != lastLine) continue search
if (endString.slice(0, lastLine.length) != lastLine) continue search
return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),
to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))}
}

View File

@ -30,16 +30,16 @@
var lastKill = null;
function kill(cm, from, to, mayGrow, text) {
function kill(cm, from, to, ring, text) {
if (text == null) text = cm.getRange(from, to);
if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
if (ring == "grow" && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
growRingTop(text);
else
else if (ring !== false)
addToRing(text);
cm.replaceRange("", from, to, "+delete");
if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
if (ring == "grow") lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
else lastKill = null;
}
@ -151,22 +151,22 @@
return f;
}
function killTo(cm, by, dir) {
function killTo(cm, by, dir, ring) {
var selections = cm.listSelections(), cursor;
var i = selections.length;
while (i--) {
cursor = selections[i].head;
kill(cm, cursor, findEnd(cm, cursor, by, dir), true);
kill(cm, cursor, findEnd(cm, cursor, by, dir), ring);
}
}
function killRegion(cm) {
function killRegion(cm, ring) {
if (cm.somethingSelected()) {
var selections = cm.listSelections(), selection;
var i = selections.length;
while (i--) {
selection = selections[i];
kill(cm, selection.anchor, selection.head);
kill(cm, selection.anchor, selection.head, ring);
}
return true;
}
@ -276,7 +276,7 @@
// Actual keymap
var keyMap = CodeMirror.keyMap.emacs = CodeMirror.normalizeKeyMap({
"Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
"Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"), true);},
"Ctrl-K": repeated(function(cm) {
var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
var text = cm.getRange(start, end);
@ -284,7 +284,7 @@
text += "\n";
end = Pos(start.line + 1, 0);
}
kill(cm, start, end, true, text);
kill(cm, start, end, "grow", text);
}),
"Alt-W": function(cm) {
addToRing(cm.getSelection());
@ -301,14 +301,14 @@
"Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1),
"Right": move(byChar, 1), "Left": move(byChar, -1),
"Ctrl-D": function(cm) { killTo(cm, byChar, 1); },
"Delete": function(cm) { killRegion(cm) || killTo(cm, byChar, 1); },
"Ctrl-H": function(cm) { killTo(cm, byChar, -1); },
"Backspace": function(cm) { killRegion(cm) || killTo(cm, byChar, -1); },
"Ctrl-D": function(cm) { killTo(cm, byChar, 1, false); },
"Delete": function(cm) { killRegion(cm, false) || killTo(cm, byChar, 1, false); },
"Ctrl-H": function(cm) { killTo(cm, byChar, -1, false); },
"Backspace": function(cm) { killRegion(cm, false) || killTo(cm, byChar, -1, false); },
"Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1),
"Alt-D": function(cm) { killTo(cm, byWord, 1); },
"Alt-Backspace": function(cm) { killTo(cm, byWord, -1); },
"Alt-D": function(cm) { killTo(cm, byWord, 1, "grow"); },
"Alt-Backspace": function(cm) { killTo(cm, byWord, -1, "grow"); },
"Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1),
"Down": move(byLine, 1), "Up": move(byLine, -1),
@ -321,11 +321,11 @@
"Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1),
"Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1),
"Alt-K": function(cm) { killTo(cm, bySentence, 1); },
"Alt-K": function(cm) { killTo(cm, bySentence, 1, "grow"); },
"Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); },
"Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); },
"Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1),
"Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1, "grow"); },
"Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1, "grow"); },
"Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1, "grow"),
"Shift-Ctrl-Alt-2": function(cm) {
var cursor = cm.getCursor();
@ -398,7 +398,7 @@
"Ctrl-X F": "open",
"Ctrl-X U": repeated("undo"),
"Ctrl-X K": "close",
"Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); },
"Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), "grow"); },
"Ctrl-X H": "selectAll",
"Ctrl-Q Tab": repeated("insertTab"),

View File

@ -145,8 +145,8 @@
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}

View File

@ -3274,8 +3274,10 @@ function updateHeightsInViewport(cm) {
// Read and store the height of line widgets associated with the
// given line.
function updateWidgetHeight(line) {
if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i)
{ line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight } }
if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) {
var w = line.widgets[i], parent = w.node.parentNode
if (parent) { w.height = parent.offsetHeight }
} }
}
// Compute the lines that are visible in a given viewport (defaults
@ -7061,18 +7063,26 @@ function lookupKeyForEditor(cm, name, handle) {
// for bound mouse clicks.
var stopSeq = new Delayed
function dispatchKey(cm, name, e, handle) {
var seq = cm.state.keySeq
if (seq) {
if (isModifierKey(name)) { return "handled" }
stopSeq.set(50, function () {
if (cm.state.keySeq == seq) {
cm.state.keySeq = null
cm.display.input.reset()
}
})
name = seq + " " + name
if (/\'$/.test(name))
{ cm.state.keySeq = null }
else
{ stopSeq.set(50, function () {
if (cm.state.keySeq == seq) {
cm.state.keySeq = null
cm.display.input.reset()
}
}) }
if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true }
}
return dispatchKeyInner(cm, name, e, handle)
}
function dispatchKeyInner(cm, name, e, handle) {
var result = lookupKeyForEditor(cm, name, handle)
if (result == "multi")
@ -7085,10 +7095,6 @@ function dispatchKey(cm, name, e, handle) {
restartBlink(cm)
}
if (seq && !result && /\'$/.test(name)) {
e_preventDefault(e)
return true
}
return !!result
}
@ -9640,7 +9646,7 @@ CodeMirror.fromTextArea = fromTextArea
addLegacyProps(CodeMirror)
CodeMirror.version = "5.31.0"
CodeMirror.version = "5.32.1"
return CodeMirror;

View File

@ -47,8 +47,6 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
"interface": kw("class"),
"implements": C,
"namespace": C,
"module": kw("module"),
"enum": kw("module"),
// scope modifiers
"public": kw("modifier"),
@ -155,7 +153,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var kw = keywords[word]
return ret(kw.type, kw.style, word)
}
if (word == "async" && stream.match(/^\s*[\(\w]/, false))
if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\(\w]/, false))
return ret("async", "keyword", word)
}
return ret("variable", "variable", word)
@ -372,9 +370,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (isTS && value == "type") {
cx.marked = "keyword"
return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
} if (isTS && value == "declare") {
} else if (isTS && value == "declare") {
cx.marked = "keyword"
return cont(statement)
} else if (isTS && (value == "module" || value == "enum") && cx.stream.match(/^\s*\w/, false)) {
cx.marked = "keyword"
return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
} else {
return cont(pushlex("stat"), maybelabel);
}
@ -388,7 +389,6 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "class") return cont(pushlex("form"), className, poplex);
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
if (type == "module") return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
if (type == "async") return cont(statement)
if (value == "@") return cont(expression, statement)
return pass(pushlex("stat"), expression, expect(";"), poplex);
@ -438,6 +438,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
if (type == "operator") {
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
if (isTS && value == "<" && cx.stream.match(/^([^>]|<.*?>)*>\s*\(/, false))
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
if (value == "?") return cont(expression, expect(":"), expr);
return cont(expr);
}
@ -564,6 +566,18 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (value == "?") return cont(maybetype);
}
}
function mayberettype(type) {
if (isTS && type == ":") {
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
else return cont(typeexpr)
}
}
function isKW(_, value) {
if (value == "is") {
cx.marked = "keyword"
return cont()
}
}
function typeexpr(type, value) {
if (type == "variable" || value == "void") {
if (value == "keyof") {
@ -607,6 +621,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function maybeTypeArgs(_, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
}
function typeparam() {
return pass(typeexpr, maybeTypeDefault)
}
function maybeTypeDefault(_, value) {
if (value == "=") return cont(typeexpr)
}
function vardef() {
return pass(pattern, maybetype, maybeAssign, vardefCont);
}
@ -660,8 +680,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function functiondef(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, functiondef)
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
}
function funarg(type, value) {
if (value == "@") cont(expression, funarg)
@ -677,7 +697,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "variable") {register(value); return cont(classNameAfter);}
}
function classNameAfter(type, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, classNameAfter)
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
if (value == "extends" || value == "implements" || (isTS && type == ","))
return cont(isTS ? typeexpr : expression, classNameAfter);
if (type == "{") return cont(pushlex("}"), classBody, poplex);

View File

@ -87,7 +87,6 @@ http://ethanschoonover.com/solarized/img/solarized-palette.png
text-decoration: underline;
text-decoration-style: dotted;
}
.cm-s-solarized .cm-strong { color: #eee; }
.cm-s-solarized .cm-error,
.cm-s-solarized .cm-invalidchar {
color: #586e75;