diff --git a/.editorconfig b/.editorconfig index aaaa7a4b..690f0125 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,6 @@ root = true indent_style = space indent_size = 2 tab_width = 2 -end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/.eslintrc b/.eslintrc index f71971cd..24b34542 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,43 +1,59 @@ # https://github.com/eslint/eslint/blob/master/docs/rules/README.md parserOptions: - ecmaVersion: 2017 + ecmaVersion: 2015 env: browser: true - commonjs: true es6: true webextensions: true globals: - CodeMirror: false - runTryCatch: true - getStyles: true - updateIcon: true - saveStyle: true - invalidateCache: true - getDatabase: true + # messaging.js + KEEP_CHANNEL_OPEN: false + FIREFOX: false + OPERA: false + URLS: false + BG: false + notifyAllTabs: false + getTab: false + getActiveTab: false + getActiveTabRealURL: false + getTabRealURL: false + openURL: false + activateTab: false + stringAsRegExp: false + ignoreChromeError: false + tryCatch: false + tryRegExp: false + tryJSONparse: false + debounce: false + deepCopy: false + onBackgroundReady: false + deleteStyleSafe: false + getStylesSafe: false + saveStyleSafe: false + sessionStorageHash: false + download: false + # localization.js + template: false + t: false + o: false + tE: false + tHTML: false + tNodeList: false + tDocLoader: false + # dom.js + onDOMready: false + scrollElementIntoView: false + enforceInputRange: false + animateElement: false + $: false + $$: false + $element: false + # prefs.js prefs: false - reportError: true - getActiveTab: true - t: true - getCodeMirrorThemes: true - setupLivePrefs: true - sessionStorageHash: true - template: true - tE: true - tHTML: true - CSSLint: true - enableStyle: true - deleteStyle: true - getType: true - importStyles: true - getActiveTabRealURL: true - getDomains: true - webSqlStorage: true - notifyAllTabs: true - handleUpdate: true - handleDelete: true + setupLivePrefs: false rules: accessor-pairs: [2] @@ -47,7 +63,7 @@ rules: arrow-parens: [2, as-needed] arrow-spacing: [2, {before: true, after: true}] block-scoped-var: [2] - brace-style: [2, 1tbs, {allowSingleLine: true}] + brace-style: [2, 1tbs, {allowSingleLine: false}] camelcase: [2, {properties: never}] class-methods-use-this: [2] comma-dangle: [0] @@ -68,16 +84,15 @@ rules: func-names: [0] generator-star-spacing: [2, before] global-require: [0] - guard-for-in: [2] + guard-for-in: [0] # not needed for our non-OOP stuff handle-callback-err: [2, ^(err|error)$] id-blacklist: [0] id-length: [0] id-match: [0] - indent: [2, 2, {VariableDeclarator: 0}] + indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}] jsx-quotes: [0] key-spacing: [0] keyword-spacing: [2] - linebreak-style: [2, unix] lines-around-comment: [0] lines-around-directive: [0] max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}] @@ -98,7 +113,7 @@ rules: no-case-declarations: [2] no-class-assign: [2] no-cond-assign: [2, except-parens] - no-confusing-arrow: [2] + no-confusing-arrow: [1, {allowParens: true}] no-const-assign: [2] no-constant-condition: [0] no-continue: [0] @@ -125,11 +140,11 @@ rules: no-extra-label: [0] no-extra-parens: [0] no-extra-semi: [2] - no-fallthrough: [2] + no-fallthrough: [2, {commentPattern: fallthrough.*}] no-floating-decimal: [0] no-func-assign: [2] no-global-assign: [2] - no-implicit-coercion: [2] + no-implicit-coercion: [1] no-implicit-globals: [0] no-implied-eval: [2] no-inline-comments: [0] @@ -139,7 +154,7 @@ rules: no-irregular-whitespace: [2] no-iterator: [2] no-label-var: [2] - no-labels: [2] + no-labels: [2, {allowLoop: true}] no-lone-blocks: [2] no-lonely-if: [0] no-loop-func: [0] @@ -149,7 +164,7 @@ rules: no-mixed-spaces-and-tabs: [2] no-multi-spaces: [0] no-multi-str: [2] - no-multiple-empty-lines: [2, {max: 1, maxEOF: 0, maxBOF: 0}] + no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}] no-native-reassign: [2] no-negated-condition: [0] no-negated-in-lhs: [2] @@ -184,43 +199,44 @@ rules: no-tabs: [2] no-template-curly-in-string: [2] no-this-before-super: [2] - no-throw-literal: [2] + no-throw-literal: [0] no-trailing-spaces: [2] no-undef-init: [2] no-undef: [2] no-undefined: [0] no-underscore-dangle: [0] no-unexpected-multiline: [2] - no-unmodified-loop-condition: [2] + no-unmodified-loop-condition: [1] no-unneeded-ternary: [2] no-unreachable: [2] no-unsafe-finally: [2] no-unsafe-negation: [2] no-unused-expressions: [2] no-unused-labels: [0] - no-unused-vars: [2, {args: all, varsIgnorePattern: clearError, argsIgnorePattern: ^_}] + no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}] no-use-before-define: [2, nofunc] no-useless-call: [2] no-useless-computed-key: [2] no-useless-concat: [2] no-useless-constructor: [2] no-useless-escape: [2] - no-var: [0] + no-var: [1] no-warning-comments: [0] no-whitespace-before-property: [2] no-with: [2] object-curly-newline: [0] object-curly-spacing: [2, never] object-shorthand: [0] - one-var-declaration-per-line: [0] + one-var-declaration-per-line: [1] one-var: [0] operator-assignment: [2, always] - operator-linebreak: [2, after] + operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}] padded-blocks: [2, never] prefer-numeric-literals: [2] prefer-rest-params: [0] + prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}] quote-props: [0] - quotes: [2, double, avoid-escape] + quotes: [1, single, avoid-escape] radix: [2, as-needed] require-jsdoc: [0] require-yield: [2] @@ -233,7 +249,7 @@ rules: space-in-parens: [2, never] space-infix-ops: [2] space-unary-ops: [2] - spaced-comment: [2, always, {markers: ["!"]}] + spaced-comment: [0, always, {markers: ["!"]}] strict: [2, global] symbol-description: [2] template-curly-spacing: [2, never] diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index 1b705ea3..50c8f547 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "هل تريد بالتأكيد حذف هذا النمط؟", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "إزالة القسم", "description": "Label for the button to remove a section" diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 9320ec0e..2cf3eebe 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -7,10 +7,6 @@ "message": "výchozí", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Pouze upravené styly.", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Exportovat", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Získat styly na userstyles.org | Získat pomoc", + "message": "Získat styly na userstyles.org | Získat pomoc", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Opravdu chcete tento styl smazat?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla Formát", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus nefunguje na těchto stránkách.)", + "stylusUnavailableForURL": { + "message": "Stylus nefunguje na těchto stránkách.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/de/messages.json b/_locales/de/messages.json index e3b2458d..e924449f 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -11,10 +11,6 @@ "message": "Styles Exportieren", "description": "" }, - "manageOnlyEdited": { - "message": "Nur bearbeitete Styles", - "description": "Checkbox to show only locally edited styles" - }, "optionsUpdateInterval": { "message": "Automatischer Update- und Installations-Intervall (in Stunden)", "description": "" @@ -83,7 +79,7 @@ "message": "Hilfe", "description": "Alternate text for help buttons" }, - "confirmOK": { + "confirmDelete": { "message": "Löschen", "description": "" }, @@ -169,7 +165,7 @@ "description": "" }, "manageText": { - "message": "Styles von userstyles.org beziehen
Hilfeseite anzeigen", + "message": "Styles von userstyles.org beziehen | Hilfeseite anzeigen", "description": "Help text on the manage page" }, "searchStyles": { @@ -356,8 +352,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus funktioniert nicht auf Seiten wie diesen.)", + "stylusUnavailableForURL": { + "message": "Stylus funktioniert nicht auf Seiten wie diesen.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { @@ -520,4 +516,4 @@ "message": "Gestalten Sie das Web mit Stylus, einem Manager für Benutzer-Styles, um. Stylus lässt Sie ganz einfach Themes und Skins für viele beliebte Webseiten installieren.", "description": "Extension description" } -} \ No newline at end of file +} diff --git a/_locales/el/messages.json b/_locales/el/messages.json index eceed4da..ac685072 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Μόνο επεξεργασμενα στυλ", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.)", + "stylusUnavailableForURL": { + "message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 127b9ff9..32fc1343 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -34,7 +34,7 @@ }, "appliesLabel": { "message": "Applies to", - "description": "Label for 'applies to' fields on the edit\/add screen" + "description": "Label for 'applies to' fields on the edit/add screen" }, "appliesRegexpOption": { "message": "URLs matching the regexp", @@ -76,6 +76,13 @@ "message": "Check all styles for updates", "description": "Label for the button to check all styles for updates" }, + "checkAllUpdatesForce": { + "message": "Check again, I didn't edit any styles!", + "description": "Label for the button to apply all detected updates" + }, + "updateCheckHistory": { + "message": "History of update checks" + }, "checkForUpdate": { "message": "Check for update", "description": "Label for the button to check a single style for an update" @@ -108,6 +115,26 @@ "message": "Theme", "description": "Label for the style editor's CSS theme." }, + "cm_matchHighlight": { + "message": "Highlight", + "description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor." + }, + "cm_matchHighlightToken": { + "message": "Token under cursor", + "description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of of the word/token under cursor even if nothing is selected" + }, + "cm_matchHighlightSelection": { + "message": "Selection only", + "description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text" + }, + "genericDisabledLabel": { + "message": "Disabled", + "description": "Used in various lists/options to indicate that something is disabled" + }, + "genericHistoryLabel": { + "message": "History", + "description": "Used in various places to show a history log of something" + }, "confirmNo": { "message": "No", "description": "'No' button in a confirm dialog" @@ -141,6 +168,9 @@ "description": "Drag'n'drop message" }, "confirmOK": { + "message": "OK" + }, + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -191,6 +221,10 @@ "message": "Enable", "description": "Label for the button to enable a style" }, + "editDeleteText": { + "message": "Delete", + "description": "Label for the context menu item in the editor to delete selected text" + }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -211,6 +245,46 @@ "message": "Type a command name", "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" }, + "importReportLegendAdded": { + "message": "added", + "description": "Text after the number of styles added in the report shown after importing styles" + }, + "importReportLegendIdentical": { + "message": "identical skipped", + "description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles" + }, + "importReportLegendInvalid": { + "message": "invalid skipped", + "description": "Text after the number of styles skipped due to being invalid (not a Stylus/Stylish backup file probably) in the report shown after importing styles" + }, + "importReportLegendUpdatedBoth": { + "message": "updated both meta info and code", + "description": "Text after the number of styles updated entirely in the report shown after importing styles" + }, + "importReportLegendUpdatedCode": { + "message": "updated code", + "description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles" + }, + "importReportLegendUpdatedMeta": { + "message": "updated meta info", + "description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles" + }, + "importReportTitle": { + "message": "Finished importing styles", + "description": "Title of the report shown after importing styles" + }, + "importReportUnchanged": { + "message": "Nothing was changed.", + "description": "Message in the report shown after importing styles" + }, + "importReportUndoneTitle": { + "message": "Import has been undone", + "description": "Title of the message box shown after undoing the import of styles" + }, + "importReportUndone": { + "message": "styles were reverted", + "description": "Text after the number of styles reverted in the message box shown after undoing the import of styles" + }, "importLabel": { "message": "Import", "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" @@ -240,7 +314,7 @@ "description": "Label for the CSSLint issues block on the style edit page" }, "issuesHelp": { - "message": "The issues found by CSSLint<\/a> with these rules enabled:", + "message": "The issues found by CSSLint with these rules enabled:", "description": "Help popup message for the CSSLint issues block on the style edit page" }, "manageFilters": { @@ -255,12 +329,40 @@ "message": "Only enabled styles", "description": "Checkbox to show only enabled styles" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" + "manageOnlyLocal": { + "message": "Only locally created styles", + "description": "Checkbox to show only locally created styles i.e. non-updatable" + }, + "manageOnlyLocalTooltip": { + "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" + }, + "manageOnlyUpdates": { + "message": "Only with updates or issues", + "description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed" + }, + "manageNewUI": { + "message": "New manage UI layout", + "description": "Label for the checkbox that toggles the new UI on manage page" + }, + "manageFavicons": { + "message": "Favicons in applies-to column", + "description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page" + }, + "manageFaviconsGray": { + "message": "Grayed out", + "description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page" + }, + "manageFaviconsHelp": { + "message": "Stylus uses an external service https://www.google.com/s2/favicons", + "description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page" + }, + "manageMaxTargets": { + "message": "Number of applies-to items", + "description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page" }, "manageText": { - "message": "Get styles on userstyles.org<\/a> | Get help<\/a>", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "manageTitle": { @@ -287,14 +389,6 @@ "message": "Options", "description": "Go to Options UI" }, - "openOptionsShortcuts": { - "message": "Shortcuts", - "description": "Go to shortcut configuration" - }, - "openShortcutsPopup": { - "message": "Shortcuts", - "description": "Go to shortcut configuration" - }, "optionsHeading": { "message": "Options", "description": "Heading for options section on manage page." @@ -304,11 +398,11 @@ "description": "Subheading for options section on manage page." }, "popupStylesFirst": { - "message": "List styles before commands in the toolbar button menu", - "description": "Label for the checkbox controlling section order in the toolbar button menu." + "message": "Styles before commands", + "description": "Label for the checkbox controlling section order in the popup." }, "prefShowBadge": { - "message": "Show number of styles active for the current site on the toolbar button", + "message": "Number of styles active for the current site", "description": "Label for the checkbox controlling toolbar badge text." }, "replace": { @@ -351,10 +445,55 @@ "message": "Remove section", "description": "Label for the button to remove a section" }, + "shortcuts": { + "message": "Shortcuts", + "description": "Go to shortcut configuration" + }, + "shortcutsNote": { + "message": "Define keyboard shortcuts" + }, "styleBadRegexp": { "message": "Regexp is invalid.", "description": "Validation message for a bad regexp in a style" }, + "styleRegexpTestButton": { + "message": "RegExp test", + "description": "RegExp test button label in the editor shown when applies-to list has a regexp value" + }, + "styleRegexpTestTitle": { + "message": "List of matching opened tabs (click on URL to focus its tab)", + "description": "RegExp test report: title of the report" + }, + "styleRegexpTestFull": { + "message": "Matching tabs", + "description": "RegExp test report: label for the fully matching expressions" + }, + "styleRegexpTestPartial": { + "message": "Not matching fully, hence skipped", + "description": "RegExp test report: label for the partially matching expressions" + }, + "styleRegexpTestNone": { + "message": "No matching tabs", + "description": "RegExp test report: label for expressions that didn't match any tabs" + }, + "styleRegexpTestInvalid": { + "message": "Invalid regexps skipped", + "description": "RegExp test report: label for the invalid expressions" + }, + "styleRegexpPartialExplanation": { + "message": "This style uses partially matching regexps in violation of CSS4 @document specification which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)." + }, + "styleRegexpInvalidExplanation": { + "message": "Some 'regexp()' rules that could not be compiled at all." + }, + "styleNotAppliedRegexpProblemTooltip": { + "message": "Style was not applied due to its incorrect usage of 'regexp()'", + "description": "Tooltip in the popup for styles that were not applied at all" + }, + "styleRegexpProblemTooltip": { + "message": "Number of sections not applied due to incorrect usage of 'regexp()'", + "description": "Tooltip in the popup for styles that were applied only partially" + }, "styleBeautify": { "message": "Beautify", "description": "Label for the CSS-beautifier button on the edit style page" @@ -417,10 +556,18 @@ } } }, - "stylishUnavailableForURL": { - "message": "(Stylus can't affect this page.)", + "stylusUnavailableForURL": { + "message": "Stylus doesn't work on pages like this.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, + "stylusUnavailableForURLdetails": { + "message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version or about:addons) as well as other extensions' pages. Chrome/Chromium forks also restrict the Chrome Web Store.", + "description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect" + }, + "toggleStyle": { + "message": "Toggle style", + "description": "Label for the checkbox to enable/disable a style" + }, "undo": { "message": "Undo", "description": "Button label" @@ -429,6 +576,14 @@ "message": "Undo (global)", "description": "CSS-beautify global Undo button label" }, + "unreachableContentScript": { + "message": "Could not communicate with the page. Try reloading the tab.", + "description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus" + }, + "unreachableFileHint": { + "message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.", + "description": "Note in the toolbar popup for file:// URLs" + }, "updateCheckFailBadResponseCode": { "message": "Update failed - server responded with code $code$.", "description": "Text that displays when an update check failed because the response code indicates an error", @@ -442,18 +597,42 @@ "message": "Update failed - server unreachable.", "description": "Text that displays when an update check failed because the update server is unreachable" }, + "updateCheckSkippedLocallyEdited": { + "message": "This style was edited locally.", + "description": "Text that displays when an update check skipped updating the style to avoid losing local modifications" + }, + "updateCheckSkippedMaybeLocallyEdited": { + "message": "This style might have been edited locally.", + "description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications" + }, + "updateCheckManualUpdateForce": { + "message": "Install update (local edits will be overwritten)", + "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" + }, + "updateCheckManualUpdateHint": { + "message": "Forcing an update will overwrite any local edits.", + "description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications" + }, "updateCheckSucceededNoUpdate": { "message": "Style is up to date.", "description": "Text that displays when an update check completed and no update is available" }, "updateAllCheckSucceededNoUpdate": { - "message": "All styles are up to date.", + "message": "No updates found.", + "description": "Text that displays when an update all check completed and no updates are available" + }, + "updateAllCheckSucceededSomeEdited": { + "message": "Some updatable styles weren't checked to avoid losing possible local edits. Updates can be forced by checking individually, or by running another check for all styles (local edits will be overwritten).", "description": "Text that displays when an update all check completed and no updates are available" }, "updateCompleted": { "message": "Update completed.", "description": "Text that displays when an update completed" }, + "updatesCurrentlyInstalled": { + "message": "Updates installed:", + "description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates." + }, "writeStyleFor": { "message": "Write style for: ", "description": "Label for toolbar pop-up that precedes the links to write a new style" @@ -469,10 +648,10 @@ "message": "Import styles" }, "optionsBadgeNormal": { - "message": "Badge background color" + "message": "Background color" }, "optionsBadgeDisabled": { - "message": "Badge background color (when disabled)" + "message": "Background color when disabled" }, "optionsPopupWidth": { "message": "Popup width (in pixels)" @@ -481,19 +660,43 @@ "message": "Automatically check for and install all available userstyle updates (in hrs)" }, "optionsUpdateIntervalNote": { - "message": "To disable the automatic userstyle update checks, set interval to zero" + "message": "To disable the automatic userstyle update checks, set interval to 0" }, - "optionsCustomize": { - "message": "UI Customizations" + "optionsUpdateImportNote": { + "message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated." + }, + "optionsCustomizeBadge": { + "message": "Badge on the toolbar icon" + }, + "optionsCustomizePopup": { + "message": "Popup" + }, + "optionsCustomizeUpdate": { + "message": "Updates" + }, + "optionsAdvanced": { + "message": "Advanced" + }, + "optionsAdvancedExposeIframes": { + "message": "Expose iframes via HTML[stylus-iframe]" + }, + "optionsAdvancedExposeIframesNote": { + "message": "Enables writing iframe-specific CSS like 'html[stylus-iframe] h1 { display:none }'" + }, + "optionsAdvancedContextDelete": { + "message": "Add 'Delete' in editor context menu" }, "optionsActions": { "message": "Actions" }, - "optionsOpenManager": { - "message": "Open styles manager" + "optionsReset": { + "message": "Reset the options to default values" }, - "optionsOpenManagerNote": { - "message": "Define a keyboard shortcut" + "optionsResetButton": { + "message": "Reset options" + }, + "optionsOpenManager": { + "message": "Manage styles" }, "optionsCheckUpdate": { "message": "Check for and install all available updates" @@ -502,6 +705,6 @@ "message": "Open" }, "optionsCheck": { - "message": "Check" + "message": "Update styles" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 71c2a973..5ac6f5d1 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -11,10 +11,6 @@ "message": "Exportar estilos", "description": "" }, - "manageOnlyEdited": { - "message": "Sólo estilos editados", - "description": "Checkbox to show only locally edited styles" - }, "optionsUpdateInterval": { "message": "Buscar e instalar automáticamente todas las actualizaciones disponibles de estilos de usuario (en horas)", "description": "" @@ -157,7 +153,7 @@ "description": "" }, "manageText": { - "message": "Obtener estilos en userstyles.org | Obtener ayuda", + "message": "Obtener estilos en userstyles.org | Obtener ayuda", "description": "Help text on the manage page" }, "searchStyles": { @@ -212,7 +208,7 @@ "message": "¿Está seguro de que quiere eliminar este estilo?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -350,8 +346,8 @@ "message": "Formato Mozilla", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus no funciona en páginas como esta)", + "stylusUnavailableForURL": { + "message": "Stylus no funciona en páginas como esta", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index 68152cf8..9d7e0ea8 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Oletko varma että haluat poistaa tämän tyylin?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Poista osio", "description": "Label for the button to remove a section" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 852833a5..b399a0d5 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -7,10 +7,6 @@ "message": "défaut", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Exportez", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Voulez-vous vraiment supprimer ce style ?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus ne fonctionne pas sur les pages de ce genre)", + "stylusUnavailableForURL": { + "message": "Stylus ne fonctionne pas sur les pages de ce genre", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 70d200ff..3a485c49 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Vuoi eliminare questo stile?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Rimuovi sezione", "description": "Label for the button to remove a section" diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index d33ce230..db2a6210 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "このスタイルを削除してもよろしいですか?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "セクションを削除", "description": "Label for the button to remove a section" diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 180a599a..618b81bd 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -7,10 +7,6 @@ "message": "standaard", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Alleen bewerkte stijlen", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Exporteren", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Verkrijg stijlen op userstyles.org | Verkrijg hulp", + "message": "Verkrijg stijlen op userstyles.org | Verkrijg hulp", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Weet u zeker dat u deze stijl wilt verwijderen?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla-opmaak", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus werkt niet op pagina's als deze.)", + "stylusUnavailableForURL": { + "message": "Stylus werkt niet op pagina's als deze.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 494549bc..729f3dd4 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Tem certeza de que deseja excluir este estilo?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Remover seção", "description": "Label for the button to remove a section" diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 1b90fce5..1b5797c2 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -7,10 +7,6 @@ "message": "по-умолчанию", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Только отредактированные стили", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Экспорт", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Скачать стили с userstyles.org | Справка", + "message": "Скачать стили с userstyles.org | Справка", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Удалить этот стиль?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Формат Mozilla", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus не работает на таких страницах)", + "stylusUnavailableForURL": { + "message": "Stylus не работает на таких страницах.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 1df5e9de..01682cad 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -1,425 +1,421 @@ -{ - "appliesToEverything": { - "message": "Све", - "description": "Text displayed for styles that apply to all sites" - }, - "defaultTheme": { - "message": "подразумевано", - "description": "Default CodeMirror CSS theme option on the edit style page" - }, - "manageOnlyEdited": { - "message": "Само уређени стилови", - "description": "Checkbox to show only locally edited styles" - }, - "exportLabel": { - "message": "Извези", - "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" - }, - "issues": { - "message": "Проблеми", - "description": "Label for the CSSLint issues block on the style edit page" - }, - "cm_tabSize": { - "message": "Величина картице", - "description": "Label for the text box controlling tab size option for the style editor." - }, - "enableStyleLabel": { - "message": "Омогући", - "description": "Label for the button to enable a style" - }, - "styleMissingName": { - "message": "Унесите назив", - "description": "Error displayed when user saves without providing a name" - }, - "appliesDomainOption": { - "message": "УРЛ адресе на домену", - "description": "Option to make the style apply to the entered string as a domain" - }, - "checkForUpdate": { - "message": "Проверите ажурирање", - "description": "Label for the button to check a single style for an update" - }, - "importAppendLabel": { - "message": "Додај стилу", - "description": "Label for the button to import a style and append to the existing sections" - }, - "updateAllCheckSucceededNoUpdate": { - "message": "Сви стилови су ажурирани.", - "description": "Text that displays when an update all check completed and no updates are available" - }, - "styleFromMozillaFormatPrompt": { - "message": "Налепи код у Mozilla формату", - "description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button" - }, - "helpAlt": { - "message": "Помоћ", - "description": "Alternate text for help buttons" - }, - "search": { - "message": "Претражи", - "description": "Label before the search input field in the editor shown on Ctrl-F" - }, - "confirmYes": { - "message": "Да", - "description": "'Yes' button in a confirm dialog" - }, - "findStylesForSite": { - "message": "Пронађи још стилова за овај сајт.", - "description": "Text for a link that gets a list of styles for the current site" - }, - "manageHeading": { - "message": "Инсталирани стилови", - "description": "Heading for the manage page" - }, - "styleBeautify": { - "message": " Улепшај", - "description": "Label for the CSS-beautifier button on the edit style page" - }, - "styleEnabledLabel": { - "message": "Омогућено", - "description": "Label for the enabled state of styles" - }, - "styleToMozillaFormatHelp": { - "message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.", - "description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format" - }, - "sectionAdd": { - "message": "Додај нови одељак", - "description": "Label for the button to add a section" - }, - "styleSaveLabel": { - "message": "Сачувај", - "description": "Label for save button for style editing" - }, - "confirmStop": { - "message": "Заустави", - "description": "'Stop' button in a confirm dialog" - }, - "writeStyleForURL": { - "message": "ову УРЛ адресу", - "description": "Text for link in toolbar pop-up to write a new style for the current URL" - }, - "appliesAdd": { - "message": "Додај", - "description": "Label for the button to add an 'applies' entry" - }, - "appliesRegexpOption": { - "message": "УРЛ адресе које одговарају регуларном изразу", - "description": "Option to make the style apply to the entered string as a regular expression" - }, - "styleInstall": { - "message": "Инсталирати '$stylename$' у Stylus?", - "description": "Confirmation when installing a style", - "placeholders": { - "stylename": { - "content": "$1" - } - } - }, - "manageText": { - "message": "Преузмите стилове са userstyles.org | Помоћ", - "description": "Help text on the manage page" - }, - "searchStyles": { - "message": "Претражи садржај", - "description": "Label for the search filter textbox on the Manage styles page" - }, - "disableStyleLabel": { - "message": "Онемогући", - "description": "Label for the button to disable a style" - }, - "prefShowBadge": { - "message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци", - "description": "Label for the checkbox controlling toolbar badge text." - }, - "menuShowBadge": { - "message": "Прикажи број активних стилова", - "description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." - }, - "cm_lineWrapping": { - "message": "Преламање текста", - "description": "Label for the checkbox controlling word wrap option for the style editor." - }, - "styleCancelEditLabel": { - "message": "Назад на управљање", - "description": "Label for cancel button for style editing" - }, - "styleChangesNotSaved": { - "message": "Направили сте измене овог стила које нисте сачували.", - "description": "Text for the prompt when changes are made to a style and the user tries to leave without saving" - }, - "importLabel": { - "message": "Увези", - "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" - }, - "updateCheckFailServerUnreachable": { - "message": "Ажурирање није успело - сервер није доступан.", - "description": "Text that displays when an update check failed because the update server is unreachable" - }, - "manageFilters": { - "message": "Филтери", - "description": "Label for filters container" - }, - "applyAllUpdates": { - "message": "Примени сва ажурирања", - "description": "Label for the button to apply all detected updates" - }, - "deleteStyleConfirm": { - "message": "Да ли сте сигурни да желите да избришете овај стил?", - "description": "Confirmation before deleting a style" - }, - "confirmOK": { - "message": "Delete" - }, - "confirmCancel": { - "message": "Cancel" - }, - "styleBadRegexp": { - "message": "Регуларни израз је неисправан.", - "description": "Validation message for a bad regexp in a style" - }, - "optionsHeading": { - "message": "Опције", - "description": "Heading for options section on manage page." - }, - "appliesDisplay": { - "message": "Примењује се на: $applies$", - "description": "Text on the manage screen to describe what the style applies to", - "placeholders": { - "applies": { - "content": "$1" - } - } - }, - "styleUpdate": { - "message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?", - "description": "Confirmation when updating a style", - "placeholders": { - "stylename": { - "content": "$1" - } - } - }, - "styleSectionsTitle": { - "message": "Одељци", - "description": "Title for the style sections section" - }, - "editStyleTitle": { - "message": "Уреди стил $stylename$", - "description": "Title of the page for editing styles", - "placeholders": { - "stylename": { - "content": "$1" - } - } - }, - "updateCheckSucceededNoUpdate": { - "message": "Стил је ажуриран.", - "description": "Text that displays when an update check completed and no update is available" - }, - "appliesUrlPrefixOption": { - "message": "УРЛ адресе које почињу са", - "description": "Option to make the style apply to the entered string as a URL prefix" - }, - "searchRegexp": { - "message": "Користи /re/ синтаксу за претрагу регуларним изразом", - "description": "Label after the search input field in the editor shown on Ctrl-F" - }, - "importReplaceTooltip": { - "message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил", - "description": "Label for the button to import and overwrite current style" - }, - "popupStylesFirst": { - "message": "Излистај стилове пре команди у менију дугмета на алатној траци", - "description": "Label for the checkbox controlling section order in the toolbar button menu." - }, - "sectionHelp": { - "message": "Одељци вам омогућавају да дефинишете различите делове кода који се примењују на раличите скупове УРЛ-ова у истом стилу. На пример, један исти стил може променити почетну страницу једног сајта на један начин а остатак сајта на други начин.", - "description": "Help text for sections" - }, - "noStylesForSite": { - "message": "Нема инсталираних стилова за овај сајт.", - "description": "Text displayed when no styles are installed for the current site" - }, - "appliesDisplayTruncatedSuffix": { - "message": "и још", - "description": "Text added to appliesDisplay when there are more sites for the style than are displayed" - }, - "appliesRemove": { - "message": "Уклони", - "description": "Label for the button to remove an 'applies' entry" - }, - "styleToMozillaFormatTitle": { - "message": "Стил у Mozilla формату", - "description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page" - }, - "manageTitle": { - "message": "Stylus", - "description": "Title for the manage page" - }, - "writeStyleFor": { - "message": "Упиши стил за:", - "description": "Label for toolbar pop-up that precedes the links to write a new style" - }, - "replace": { - "message": "Замени", - "description": "Label before the replace input field in the editor shown on Ctrl-H" - }, - "appliesLabel": { - "message": "Примењује се на", - "description": "Label for 'applies to' fields on the edit/add screen" - }, - "openManage": { - "message": "Управљај инсталираним стиловима", - "description": "Link to open the manage page." - }, - "updateCheckFailBadResponseCode": { - "message": "Ажурирање није успело - сервер је одговорио кодом $code$.", - "description": "Text that displays when an update check failed because the response code indicates an error", - "placeholders": { - "code": { - "content": "$1" - } - } - }, - "appliesSpecify": { - "message": "Детаљније", - "description": "Label for the button to make a style apply only to specific sites" - }, - "installUpdate": { - "message": "Инсталирај ажурирање", - "description": "Label for the button to install an update for a single style" - }, - "styleMozillaFormatHeading": { - "message": "Mozilla формат", - "description": "Heading for the section with buttons to import/export Mozilla format of the style" - }, - "stylishUnavailableForURL": { - "message": "(Stylus не ради на страницама као што је ова.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, - "sectionRemove": { - "message": "Уклони одељак", - "description": "Label for the button to remove a section" - }, - "disableAllStyles": { - "message": "Искључи све стилове", - "description": "Label for the checkbox that turns all enabled styles off." - }, - "undoGlobal": { - "message": "Опозови (свеобухватно)", - "description": "CSS-beautify global Undo button label" - }, - "updateCompleted": { - "message": "Ажурирање је комплетирано.", - "description": "Text that displays when an update completed" - }, - "checkingForUpdate": { - "message": "Проверавање...", - "description": "Text to display when checking a style for an update" - }, - "sectionCode": { - "message": "Код", - "description": "Label for the code for a section" - }, - "cm_smartIndent": { - "message": "Користи паметно увлачење редова", - "description": "Label for the checkbox controlling smart indentation option for the style editor." - }, - "appliesHelp": { - "message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.", - "description": "Help text for 'applies to' section" - }, - "editStyleHeading": { - "message": "Уреди стил", - "description": "Title of the page for editing styles" - }, - "appliesUrlOption": { - "message": "УРЛ", - "description": "Option to make the style apply to the entered string as a URL" - }, - "addStyleTitle": { - "message": "Додај стил", - "description": "Title of the page for adding styles" - }, - "importReplaceLabel": { - "message": "Упиши преко стила", - "description": "Label for the button to import and overwrite current style" - }, - "dbError": { - "message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?", - "description": "Prompt when a DB error is encountered" - }, - "importAppendTooltip": { - "message": "Додај увезени стил тренутном стилу", - "description": "Tooltip for the button to import a style and append to the existing sections" - }, - "helpKeyMapHotkey": { - "message": "Притисни пречицу", - "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" - }, - "replaceAll": { - "message": "Замени све", - "description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey" - }, - "editGotoLine": { - "message": "Иди на ред (или line:col)", - "description": "Go to line or line:column on Ctrl-G in style code editor" - }, - "checkAllUpdates": { - "message": "Проверите ажурирања за све стилове", - "description": "Label for the button to check all styles for updates" - }, - "issuesHelp": { - "message": "Проблем пронађен од стране CSSLint са овим омогућеним правилима:", - "description": "Help popup message for the CSSLint issues block on the style edit page" - }, - "confirmNo": { - "message": "Не", - "description": "'No' button in a confirm dialog" - }, - "undo": { - "message": "Опозови", - "description": "Button label" - }, - "cm_keyMap": { - "message": "Мапа тастера", - "description": "Label for the drop-down list controlling the keymap for the style editor." - }, - "cm_indentWithTabs": { - "message": "Користи картице са паметним увлачењем редова", - "description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." - }, - "replaceWith": { - "message": "Замени са", - "description": "Label before the replace-with input field in the editor shown on Ctrl-H etc." - }, - "deleteStyleLabel": { - "message": "Избриши", - "description": "Label for the button to delete a style" - }, - "addStyleLabel": { - "message": "Упиши нови стил", - "description": "Label for the button to go to the add style page" - }, - "manageOnlyEnabled": { - "message": "Само омогућени стилови", - "description": "Checkbox to show only enabled styles" - }, - "editStyleLabel": { - "message": "Уреди", - "description": "Label for the button to go to the edit style page" - }, - "cm_theme": { - "message": "Тема", - "description": "Label for the style editor's CSS theme." - }, - "helpKeyMapCommand": { - "message": "Укуцај име команде", - "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" - }, - "description": { - "message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.", - "description": "Extension description" - } -} +{ + "appliesToEverything": { + "message": "Све", + "description": "Text displayed for styles that apply to all sites" + }, + "defaultTheme": { + "message": "подразумевано", + "description": "Default CodeMirror CSS theme option on the edit style page" + }, + "exportLabel": { + "message": "Извези", + "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" + }, + "issues": { + "message": "Проблеми", + "description": "Label for the CSSLint issues block on the style edit page" + }, + "cm_tabSize": { + "message": "Величина картице", + "description": "Label for the text box controlling tab size option for the style editor." + }, + "enableStyleLabel": { + "message": "Омогући", + "description": "Label for the button to enable a style" + }, + "styleMissingName": { + "message": "Унесите назив", + "description": "Error displayed when user saves without providing a name" + }, + "appliesDomainOption": { + "message": "УРЛ адресе на домену", + "description": "Option to make the style apply to the entered string as a domain" + }, + "checkForUpdate": { + "message": "Проверите ажурирање", + "description": "Label for the button to check a single style for an update" + }, + "importAppendLabel": { + "message": "Додај стилу", + "description": "Label for the button to import a style and append to the existing sections" + }, + "updateAllCheckSucceededNoUpdate": { + "message": "Сви стилови су ажурирани.", + "description": "Text that displays when an update all check completed and no updates are available" + }, + "styleFromMozillaFormatPrompt": { + "message": "Налепи код у Mozilla формату", + "description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button" + }, + "helpAlt": { + "message": "Помоћ", + "description": "Alternate text for help buttons" + }, + "search": { + "message": "Претражи", + "description": "Label before the search input field in the editor shown on Ctrl-F" + }, + "confirmYes": { + "message": "Да", + "description": "'Yes' button in a confirm dialog" + }, + "findStylesForSite": { + "message": "Пронађи још стилова за овај сајт.", + "description": "Text for a link that gets a list of styles for the current site" + }, + "manageHeading": { + "message": "Инсталирани стилови", + "description": "Heading for the manage page" + }, + "styleBeautify": { + "message": " Улепшај", + "description": "Label for the CSS-beautifier button on the edit style page" + }, + "styleEnabledLabel": { + "message": "Омогућено", + "description": "Label for the enabled state of styles" + }, + "styleToMozillaFormatHelp": { + "message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.", + "description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format" + }, + "sectionAdd": { + "message": "Додај нови одељак", + "description": "Label for the button to add a section" + }, + "styleSaveLabel": { + "message": "Сачувај", + "description": "Label for save button for style editing" + }, + "confirmStop": { + "message": "Заустави", + "description": "'Stop' button in a confirm dialog" + }, + "writeStyleForURL": { + "message": "ову УРЛ адресу", + "description": "Text for link in toolbar pop-up to write a new style for the current URL" + }, + "appliesAdd": { + "message": "Додај", + "description": "Label for the button to add an 'applies' entry" + }, + "appliesRegexpOption": { + "message": "УРЛ адресе које одговарају регуларном изразу", + "description": "Option to make the style apply to the entered string as a regular expression" + }, + "styleInstall": { + "message": "Инсталирати '$stylename$' у Stylus?", + "description": "Confirmation when installing a style", + "placeholders": { + "stylename": { + "content": "$1" + } + } + }, + "manageText": { + "message": "Преузмите стилове са userstyles.org | Помоћ", + "description": "Help text on the manage page" + }, + "searchStyles": { + "message": "Претражи садржај", + "description": "Label for the search filter textbox on the Manage styles page" + }, + "disableStyleLabel": { + "message": "Онемогући", + "description": "Label for the button to disable a style" + }, + "prefShowBadge": { + "message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци", + "description": "Label for the checkbox controlling toolbar badge text." + }, + "menuShowBadge": { + "message": "Прикажи број активних стилова", + "description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." + }, + "cm_lineWrapping": { + "message": "Преламање текста", + "description": "Label for the checkbox controlling word wrap option for the style editor." + }, + "styleCancelEditLabel": { + "message": "Назад на управљање", + "description": "Label for cancel button for style editing" + }, + "styleChangesNotSaved": { + "message": "Направили сте измене овог стила које нисте сачували.", + "description": "Text for the prompt when changes are made to a style and the user tries to leave without saving" + }, + "importLabel": { + "message": "Увези", + "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" + }, + "updateCheckFailServerUnreachable": { + "message": "Ажурирање није успело - сервер није доступан.", + "description": "Text that displays when an update check failed because the update server is unreachable" + }, + "manageFilters": { + "message": "Филтери", + "description": "Label for filters container" + }, + "applyAllUpdates": { + "message": "Примени сва ажурирања", + "description": "Label for the button to apply all detected updates" + }, + "deleteStyleConfirm": { + "message": "Да ли сте сигурни да желите да избришете овај стил?", + "description": "Confirmation before deleting a style" + }, + "confirmDelete": { + "message": "Delete" + }, + "confirmCancel": { + "message": "Cancel" + }, + "styleBadRegexp": { + "message": "Регуларни израз је неисправан.", + "description": "Validation message for a bad regexp in a style" + }, + "optionsHeading": { + "message": "Опције", + "description": "Heading for options section on manage page." + }, + "appliesDisplay": { + "message": "Примењује се на: $applies$", + "description": "Text on the manage screen to describe what the style applies to", + "placeholders": { + "applies": { + "content": "$1" + } + } + }, + "styleUpdate": { + "message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?", + "description": "Confirmation when updating a style", + "placeholders": { + "stylename": { + "content": "$1" + } + } + }, + "styleSectionsTitle": { + "message": "Одељци", + "description": "Title for the style sections section" + }, + "editStyleTitle": { + "message": "Уреди стил $stylename$", + "description": "Title of the page for editing styles", + "placeholders": { + "stylename": { + "content": "$1" + } + } + }, + "updateCheckSucceededNoUpdate": { + "message": "Стил је ажуриран.", + "description": "Text that displays when an update check completed and no update is available" + }, + "appliesUrlPrefixOption": { + "message": "УРЛ адресе које почињу са", + "description": "Option to make the style apply to the entered string as a URL prefix" + }, + "searchRegexp": { + "message": "Користи /re/ синтаксу за претрагу регуларним изразом", + "description": "Label after the search input field in the editor shown on Ctrl-F" + }, + "importReplaceTooltip": { + "message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил", + "description": "Label for the button to import and overwrite current style" + }, + "popupStylesFirst": { + "message": "Излистај стилове пре команди у менију дугмета на алатној траци", + "description": "Label for the checkbox controlling section order in the toolbar button menu." + }, + "sectionHelp": { + "message": "Одељци вам омогућавају да дефинишете различите делове кода који се примењују на раличите скупове УРЛ-ова у истом стилу. На пример, један исти стил може променити почетну страницу једног сајта на један начин а остатак сајта на други начин.", + "description": "Help text for sections" + }, + "noStylesForSite": { + "message": "Нема инсталираних стилова за овај сајт.", + "description": "Text displayed when no styles are installed for the current site" + }, + "appliesDisplayTruncatedSuffix": { + "message": "и још", + "description": "Text added to appliesDisplay when there are more sites for the style than are displayed" + }, + "appliesRemove": { + "message": "Уклони", + "description": "Label for the button to remove an 'applies' entry" + }, + "styleToMozillaFormatTitle": { + "message": "Стил у Mozilla формату", + "description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page" + }, + "manageTitle": { + "message": "Stylus", + "description": "Title for the manage page" + }, + "writeStyleFor": { + "message": "Упиши стил за:", + "description": "Label for toolbar pop-up that precedes the links to write a new style" + }, + "replace": { + "message": "Замени", + "description": "Label before the replace input field in the editor shown on Ctrl-H" + }, + "appliesLabel": { + "message": "Примењује се на", + "description": "Label for 'applies to' fields on the edit/add screen" + }, + "openManage": { + "message": "Управљај инсталираним стиловима", + "description": "Link to open the manage page." + }, + "updateCheckFailBadResponseCode": { + "message": "Ажурирање није успело - сервер је одговорио кодом $code$.", + "description": "Text that displays when an update check failed because the response code indicates an error", + "placeholders": { + "code": { + "content": "$1" + } + } + }, + "appliesSpecify": { + "message": "Детаљније", + "description": "Label for the button to make a style apply only to specific sites" + }, + "installUpdate": { + "message": "Инсталирај ажурирање", + "description": "Label for the button to install an update for a single style" + }, + "styleMozillaFormatHeading": { + "message": "Mozilla формат", + "description": "Heading for the section with buttons to import/export Mozilla format of the style" + }, + "stylusUnavailableForURL": { + "message": "Stylus не ради на страницама као што је ова.", + "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" + }, + "sectionRemove": { + "message": "Уклони одељак", + "description": "Label for the button to remove a section" + }, + "disableAllStyles": { + "message": "Искључи све стилове", + "description": "Label for the checkbox that turns all enabled styles off." + }, + "undoGlobal": { + "message": "Опозови (свеобухватно)", + "description": "CSS-beautify global Undo button label" + }, + "updateCompleted": { + "message": "Ажурирање је комплетирано.", + "description": "Text that displays when an update completed" + }, + "checkingForUpdate": { + "message": "Проверавање...", + "description": "Text to display when checking a style for an update" + }, + "sectionCode": { + "message": "Код", + "description": "Label for the code for a section" + }, + "cm_smartIndent": { + "message": "Користи паметно увлачење редова", + "description": "Label for the checkbox controlling smart indentation option for the style editor." + }, + "appliesHelp": { + "message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.", + "description": "Help text for 'applies to' section" + }, + "editStyleHeading": { + "message": "Уреди стил", + "description": "Title of the page for editing styles" + }, + "appliesUrlOption": { + "message": "УРЛ", + "description": "Option to make the style apply to the entered string as a URL" + }, + "addStyleTitle": { + "message": "Додај стил", + "description": "Title of the page for adding styles" + }, + "importReplaceLabel": { + "message": "Упиши преко стила", + "description": "Label for the button to import and overwrite current style" + }, + "dbError": { + "message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?", + "description": "Prompt when a DB error is encountered" + }, + "importAppendTooltip": { + "message": "Додај увезени стил тренутном стилу", + "description": "Tooltip for the button to import a style and append to the existing sections" + }, + "helpKeyMapHotkey": { + "message": "Притисни пречицу", + "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" + }, + "replaceAll": { + "message": "Замени све", + "description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey" + }, + "editGotoLine": { + "message": "Иди на ред (или line:col)", + "description": "Go to line or line:column on Ctrl-G in style code editor" + }, + "checkAllUpdates": { + "message": "Проверите ажурирања за све стилове", + "description": "Label for the button to check all styles for updates" + }, + "issuesHelp": { + "message": "Проблем пронађен од стране CSSLint са овим омогућеним правилима:", + "description": "Help popup message for the CSSLint issues block on the style edit page" + }, + "confirmNo": { + "message": "Не", + "description": "'No' button in a confirm dialog" + }, + "undo": { + "message": "Опозови", + "description": "Button label" + }, + "cm_keyMap": { + "message": "Мапа тастера", + "description": "Label for the drop-down list controlling the keymap for the style editor." + }, + "cm_indentWithTabs": { + "message": "Користи картице са паметним увлачењем редова", + "description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." + }, + "replaceWith": { + "message": "Замени са", + "description": "Label before the replace-with input field in the editor shown on Ctrl-H etc." + }, + "deleteStyleLabel": { + "message": "Избриши", + "description": "Label for the button to delete a style" + }, + "addStyleLabel": { + "message": "Упиши нови стил", + "description": "Label for the button to go to the add style page" + }, + "manageOnlyEnabled": { + "message": "Само омогућени стилови", + "description": "Checkbox to show only enabled styles" + }, + "editStyleLabel": { + "message": "Уреди", + "description": "Label for the button to go to the edit style page" + }, + "cm_theme": { + "message": "Тема", + "description": "Label for the style editor's CSS theme." + }, + "helpKeyMapCommand": { + "message": "Укуцај име команде", + "description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" + }, + "description": { + "message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.", + "description": "Extension description" + } +} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index d8b22d11..5b015421 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Endast ändrade stilar", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Är du säker på att du vill ta bort denna stil?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus fungerar inte på sidor som denna.)", + "stylusUnavailableForURL": { + "message": "Stylus fungerar inte på sidor som denna.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/sv_SE/messages.json b/_locales/sv_SE/messages.json index 02743ada..1016654e 100644 --- a/_locales/sv_SE/messages.json +++ b/_locales/sv_SE/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Endast ändrade stilar", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Är du säker på att du vill ta bort denna stil?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus fungerar inte på sidor som dessa.)", + "stylusUnavailableForURL": { + "message": "Stylus fungerar inte på sidor som dessa.", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/te/messages.json b/_locales/te/messages.json index e563bc42..ed0550f2 100644 --- a/_locales/te/messages.json +++ b/_locales/te/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Remove section", "description": "Label for the button to remove a section" diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index cf2c5ce4..efbc2fd3 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "Bu stili silmek istediğinizden emin misiniz?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "Bölümü kaldır", "description": "Label for the button to remove a section" diff --git a/_locales/zh/messages.json b/_locales/zh/messages.json index d8e92d21..a28bf22c 100644 --- a/_locales/zh/messages.json +++ b/_locales/zh/messages.json @@ -7,10 +7,6 @@ "message": "default", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "Only edited styles", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "Export", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "确定要删除这个样式吗?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,10 +290,6 @@ "message": "Mozilla Format", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus does not work on pages like this.)", - "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" - }, "sectionRemove": { "message": "移除节", "description": "Label for the button to remove a section" diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 84f5761d..98cb6163 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -11,10 +11,6 @@ "message": "导出所有样式", "description": "" }, - "manageOnlyEdited": { - "message": "仅修改过的样式", - "description": "Checkbox to show only locally edited styles" - }, "optionsUpdateInterval": { "message": "每 N 小时,检查所有样式更新(0 为关闭检查)", "description": "" @@ -79,7 +75,7 @@ "message": "帮助", "description": "Alternate text for help buttons" }, - "confirmOK": { + "confirmDelete": { "message": "确定", "description": "" }, @@ -165,7 +161,7 @@ "description": "" }, "manageText": { - "message": "访问 userstyles.org 获取样式 | 获取帮助", + "message": "访问 userstyles.org 获取样式 | 获取帮助", "description": "Help text on the manage page" }, "searchStyles": { @@ -352,8 +348,8 @@ "message": "Mozilla 格式", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "(Stylus在这样的页面上不工作)", + "stylusUnavailableForURL": { + "message": "Stylus在这样的页面上不工作", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 4e3f06d8..e326dfc8 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -7,10 +7,6 @@ "message": "默認", "description": "Default CodeMirror CSS theme option on the edit style page" }, - "manageOnlyEdited": { - "message": "只顯示已禁用的樣式", - "description": "Checkbox to show only locally edited styles" - }, "exportLabel": { "message": "導出", "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" @@ -117,7 +113,7 @@ } }, "manageText": { - "message": "Get styles on userstyles.org | Get help", + "message": "Get styles on userstyles.org | Get help", "description": "Help text on the manage page" }, "searchStyles": { @@ -168,7 +164,7 @@ "message": "確定要刪除這個樣式嗎?", "description": "Confirmation before deleting a style" }, - "confirmOK": { + "confirmDelete": { "message": "Delete" }, "confirmCancel": { @@ -294,8 +290,8 @@ "message": "Mozilla格式", "description": "Heading for the section with buttons to import/export Mozilla format of the style" }, - "stylishUnavailableForURL": { - "message": "( Stylus 不能在諸如此類的網頁上生效。)", + "stylusUnavailableForURL": { + "message": "Stylus 不能在諸如此類的網頁上生效。", "description": "Note in the toolbar pop-up when on a URL Stylus can't affect" }, "sectionRemove": { diff --git a/apply.js b/apply.js index e882ed9f..6fc05cbe 100644 --- a/apply.js +++ b/apply.js @@ -1,352 +1,336 @@ -// using ES5 syntax because ES6 is fast only since around Chrome 55 -// so we'll wait until Chrome 60 arguably before converting +// Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/ +// like destructring, classes, defaults, spread, calculated key names +/* eslint no-var: 0 */ +'use strict'; -var g_disableAll = false; -var g_styleElements = {}; -var iframeObserver; -var retiredStyleIds = []; +var ID_PREFIX = 'stylus-'; +var ROOT = document.documentElement; +var isOwnPage = location.href.startsWith('chrome-extension:'); +var disableAll = false; +var exposeIframes = false; +var styleElements = new Map(); +var disabledElements = new Map(); +var retiredStyleTimers = new Map(); +var docRewriteObserver; -initObserver(); requestStyles(); - -function requestStyles() { - // If this is a Stylish page (Edit Style or Manage Styles), - // we'll request the styles directly to minimize delay and flicker, - // unless Chrome still starts up and the background page isn't fully loaded. - // (Note: in this case the function may be invoked again from applyStyles.) - var request = {method: "getStyles", matchUrl: location.href, enabled: true, asHash: true}; - if (location.href.indexOf(chrome.extension.getURL("")) == 0) { - var bg = chrome.extension.getBackgroundPage(); - if (bg && bg.getStyles) { - // apply styles immediately, then proceed with a normal request that will update the icon - bg.getStyles(request, applyStyles); - } - } - chrome.runtime.sendMessage(request, applyStyles); -} - chrome.runtime.onMessage.addListener(applyOnMessage); +if (!isOwnPage) { + window.dispatchEvent(new CustomEvent(chrome.runtime.id)); + window.addEventListener(chrome.runtime.id, orphanCheck, true); +} + +function requestStyles(options, callback = applyStyles) { + var matchUrl = location.href; + if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { + // dynamic about: and javascript: iframes don't have an URL yet + // so we'll try the parent frame which is guaranteed to have a real URL + try { + if (window != parent) { + matchUrl = parent.location.href; + } + } catch (e) {} + } + const request = Object.assign({ + method: 'getStyles', + matchUrl, + enabled: true, + asHash: true, + }, options); + // On own pages we request the styles directly to minimize delay and flicker + if (typeof getStylesSafe !== 'undefined') { + getStylesSafe(request).then(callback); + } else { + chrome.runtime.sendMessage(request, callback); + } +} + + function applyOnMessage(request, sender, sendResponse) { - // Also handle special request just for the pop-up - switch (request.method == "updatePopup" ? request.reason : request.method) { - case "styleDeleted": - removeStyle(request.id, document); - break; - case "styleUpdated": - if (request.style.enabled) { - retireStyle(request.style.id); - // fallthrough to "styleAdded" - } else { - removeStyle(request.style.id, document); - break; - } - case "styleAdded": - if (request.style.enabled) { - chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles); - } - break; - case "styleApply": - applyStyles(request.styles); - break; - case "styleReplaceAll": - replaceAll(request.styles, document); - break; - case "styleDisableAll": - disableAll(request.disableAll); - break; - case "ping": - sendResponse(true); - break; - } + if (request.styles == 'DIY') { + // Do-It-Yourself tells our built-in pages to fetch the styles directly + // which is faster because IPC messaging JSON-ifies everything internally + requestStyles({}, styles => { + request.styles = styles; + applyOnMessage(request); + }); + return; + } + + switch (request.method) { + + case 'styleDeleted': + removeStyle(request); + break; + + case 'styleUpdated': + if (request.codeIsUpdated === false) { + applyStyleState(request.style); + break; + } + if (request.style.enabled) { + removeStyle({id: request.style.id, retire: true}); + requestStyles({id: request.style.id}); + } else { + removeStyle(request.style); + } + break; + + case 'styleAdded': + if (request.style.enabled) { + requestStyles({id: request.style.id}); + } + break; + + case 'styleApply': + applyStyles(request.styles); + break; + + case 'styleReplaceAll': + replaceAll(request.styles); + break; + + case 'prefChanged': + if ('disableAll' in request.prefs) { + doDisableAll(request.prefs.disableAll); + } + if ('exposeIframes' in request.prefs) { + doExposeIframes(request.prefs.exposeIframes); + } + break; + + case 'ping': + sendResponse(true); + break; + } } -function disableAll(disable) { - if (!disable === !g_disableAll) { - return; - } - g_disableAll = disable; - if (g_disableAll) { - iframeObserver.disconnect(); - } - disableSheets(g_disableAll, document); - - if (!g_disableAll && document.readyState != "loading") { - iframeObserver.start(); - } - - function disableSheets(disable, doc) { - Array.prototype.forEach.call(doc.styleSheets, function(stylesheet) { - if (stylesheet.ownerNode.classList.contains("stylus")) { - stylesheet.disabled = disable; - } - }); - getDynamicIFrames(doc).forEach(function(iframe) { - if (!disable) { - // update the IFRAME if it was created while the observer was disconnected - addDocumentStylesToIFrame(iframe); - } - disableSheets(disable, iframe.contentDocument); - }); - } +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 removeStyle(id, doc) { - var e = doc.getElementById("stylus-" + id); - delete g_styleElements["stylus-" + id]; - if (e) { - e.remove(); - } - if (doc == document && Object.keys(g_styleElements).length == 0) { - iframeObserver.disconnect(); - } - getDynamicIFrames(doc).forEach(function(iframe) { - removeStyle(id, iframe.contentDocument); - }); + +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'); + } } -// 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 -function retireStyle(id, doc) { - var deadID = "ghost-" + id; - if (!doc) { - doc = document; - retiredStyleIds.push(deadID); - delete g_styleElements["stylus-" + id]; - // in case something went wrong and new style was never applied - setTimeout(removeStyle.bind(null, deadID, doc), 1000); - } - var e = doc.getElementById("stylus-" + id); - if (e) { - e.id = "stylus-" + deadID; - } - getDynamicIFrames(doc).forEach(function(iframe) { - retireStyle(id, iframe.contentDocument); - }); + +function applyStyleState({id, enabled}) { + const inCache = disabledElements.get(id) || styleElements.get(id); + const inDoc = document.getElementById(ID_PREFIX + id); + if (enabled) { + if (inDoc) { + return; + } else if (inCache) { + addStyleElement(inCache); + disabledElements.delete(id); + } else { + requestStyles({id}); + } + } else { + if (inDoc) { + disabledElements.set(id, inDoc); + inDoc.remove(); + } + } } -function applyStyles(styleHash) { - if (!styleHash) { // Chrome is starting up - requestStyles(); - return; - } - if ("disableAll" in styleHash) { - disableAll(styleHash.disableAll); - delete styleHash.disableAll; - } - for (var styleId in styleHash) { - applySections(styleId, styleHash[styleId]); - } - - if (Object.keys(g_styleElements).length) { - // when site response is application/xml Chrome displays our style elements - // under document.documentElement as plain text so we need to move them into HEAD - // (which already is autogenerated at this moment for the xml response) - if (document.head && document.head.firstChild && document.head.firstChild.id == "xml-viewer-style") { - for (var id in g_styleElements) { - document.head.appendChild(document.getElementById(id)); - } - } - document.addEventListener("DOMContentLoaded", onDOMContentLoaded); - } - - if (retiredStyleIds.length) { - setTimeout(function() { - while (retiredStyleIds.length) { - removeStyle(retiredStyleIds.shift(), document); - } - }, 0); - } +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 onDOMContentLoaded() { - addDocumentStylesToAllIFrames(); - iframeObserver.start(); + +function applyStyles(styles) { + if (!styles) { + // Chrome is starting up + requestStyles(); + return; + } + if ('disableAll' in styles) { + doDisableAll(styles.disableAll); + delete styles.disableAll; + } + if ('exposeIframes' in styles) { + doExposeIframes(styles.exposeIframes); + delete styles.exposeIframes; + } + if (document.head + && document.head.firstChild + && document.head.firstChild.id == 'xml-viewer-style') { + // when site response is application/xml Chrome displays our style elements + // under document.documentElement as plain text so we need to move them into HEAD + // which is already autogenerated at this moment + ROOT = document.head; + } + for (const id in styles) { + applySections(id, styles[id]); + } + initDocRewriteObserver(); + if (retiredStyleTimers.size) { + setTimeout(() => { + for (const [id, timer] of retiredStyleTimers.entries()) { + removeStyle({id}); + clearTimeout(timer); + } + }); + } } + function applySections(styleId, sections) { - var styleElement = document.getElementById("stylus-" + styleId); - // Already there. - if (styleElement) { - return; - } - if (document.documentElement instanceof SVGSVGElement) { - // SVG document, make an SVG style element. - styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style"); - } else { - // This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too. - styleElement = document.createElement("style"); - } - styleElement.setAttribute("id", "stylus-" + styleId); - styleElement.setAttribute("class", "stylus"); - styleElement.setAttribute("type", "text/css"); - styleElement.appendChild(document.createTextNode(sections.map(function(section) { - return section.code; - }).join("\n"))); - addStyleElement(styleElement, document); - g_styleElements[styleElement.id] = styleElement; + let el = document.getElementById(ID_PREFIX + styleId); + if (el) { + return; + } + if (document.documentElement instanceof SVGSVGElement) { + // SVG document style + el = document.createElementNS('http://www.w3.org/2000/svg', 'style'); + } else if (document instanceof XMLDocument) { + // XML document style + el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style'); + } else { + // HTML document style; also works on HTML-embedded SVG + el = document.createElement('style'); + } + Object.assign(el, { + id: ID_PREFIX + styleId, + className: 'stylus', + type: 'text/css', + textContent: sections.map(section => section.code).join('\n'), + }); + addStyleElement(el); + styleElements.set(el.id, el); + disabledElements.delete(styleId); } -function addStyleElement(styleElement, doc) { - if (!doc.documentElement || doc.getElementById(styleElement.id)) { - return; - } - doc.documentElement.appendChild(doc.importNode(styleElement, true)) - .disabled = g_disableAll; - getDynamicIFrames(doc).forEach(function(iframe) { - if (iframeIsLoadingSrcDoc(iframe)) { - addStyleToIFrameSrcDoc(iframe, styleElement); - } else { - addStyleElement(styleElement, iframe.contentDocument); - } - }); + +function addStyleElement(el) { + if (ROOT && !document.getElementById(el.id)) { + ROOT.appendChild(el); + el.disabled = disableAll; + } } -function addDocumentStylesToIFrame(iframe) { - var doc = iframe.contentDocument; - var srcDocIsLoading = iframeIsLoadingSrcDoc(iframe); - for (var id in g_styleElements) { - if (srcDocIsLoading) { - addStyleToIFrameSrcDoc(iframe, g_styleElements[id]); - } else { - addStyleElement(g_styleElements[id], doc); - } - } + +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 addDocumentStylesToAllIFrames() { - getDynamicIFrames(document).forEach(addDocumentStylesToIFrame); + +function initDocRewriteObserver() { + if (isOwnPage || docRewriteObserver || !styleElements.size) { + return; + } + // re-add styles if we detect documentElement being recreated + const reinjectStyles = () => { + if (!styleElements) { + return orphanCheck && orphanCheck(); + } + ROOT = document.documentElement; + for (const el of styleElements.values()) { + addStyleElement(document.importNode(el, true)); + } + }; + // detect documentElement being rewritten from inside the script + docRewriteObserver = new MutationObserver(mutations => { + for (let m = mutations.length; --m >= 0;) { + const added = mutations[m].addedNodes; + for (let n = added.length; --n >= 0;) { + if (added[n].localName == 'html') { + reinjectStyles(); + return; + } + } + } + }); + docRewriteObserver.observe(document, {childList: true}); + // detect dynamic iframes rewritten after creation by the embedder i.e. externally + setTimeout(() => { + if (document.documentElement != ROOT) { + reinjectStyles(); + } + }); } -// Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs. -function getDynamicIFrames(doc) { - return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); -} - -function iframeIsDynamic(f) { - var href; - try { - href = f.contentDocument.location.href; - } catch (ex) { - // Cross-origin, so it's not a dynamic iframe - return false; - } - return href == document.location.href || href.indexOf("about:") == 0; -} - -function iframeIsLoadingSrcDoc(f) { - return f.srcdoc && f.contentDocument.all.length <= 3; - // 3 nodes or less in total (html, head, body) == new empty iframe about to be overwritten by its 'srcdoc' -} - -function addStyleToIFrameSrcDoc(iframe, styleElement) { - if (g_disableAll) { - return; - } - iframe.srcdoc += styleElement.outerHTML; - // make sure the style is added in case srcdoc was malformed - setTimeout(addStyleElement.bind(null, styleElement, iframe.contentDocument), 100); -} - -function replaceAll(newStyles, doc, pass2) { - var oldStyles = [].slice.call(doc.querySelectorAll("STYLE.stylus" + (pass2 ? "[id$='-ghost']" : ""))); - if (!pass2) { - oldStyles.forEach(function(style) { style.id += "-ghost"; }); - } - getDynamicIFrames(doc).forEach(function(iframe) { - replaceAll(newStyles, iframe.contentDocument, pass2); - }); - if (doc == document && !pass2) { - g_styleElements = {}; - applyStyles(newStyles); - replaceAll(newStyles, doc, true); - } - if (pass2) { - oldStyles.forEach(function(style) { style.remove(); }); - } -} - -// Observe dynamic IFRAMEs being added -function initObserver() { - var orphanCheckTimer; - - iframeObserver = new MutationObserver(function(mutations) { - clearTimeout(orphanCheckTimer); - // MutationObserver runs as a microtask so the timer won't fire until all queued mutations are fired - orphanCheckTimer = setTimeout(orphanCheck, 0); - - if (mutations.length > 1000) { - // use a much faster method for very complex pages with 100,000 mutations - // (observer usually receives 1k-10k mutations per call) - addDocumentStylesToAllIFrames(); - return; - } - // move the check out of current execution context - // because some same-domain (!) iframes fail to load when their "contentDocument" is accessed (!) - // namely gmail's old chat iframe talkgadget.google.com - setTimeout(process.bind(null, mutations), 0); - }); - - function process(mutations) { - for (var m = 0, ml = mutations.length; m < ml; m++) { - var mutation = mutations[m]; - if (mutation.type === "childList") { - for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) { - var node = nodes[n]; - if (node.localName === "iframe" && iframeIsDynamic(node)) { - addDocumentStylesToIFrame(node); - } - } - } - } - } - - iframeObserver.start = function() { - // will be ignored by browser if already observing - iframeObserver.observe(document, {childList: true, subtree: true}); - } - - function orphanCheck() { - orphanCheckTimer = 0; - var port = chrome.runtime.connect(); - if (port) { - port.disconnect(); - return; - } - - // we're orphaned due to an extension update - // we can detach the mutation observer - iframeObserver.takeRecords(); - iframeObserver.disconnect(); - iframeObserver = null; - // we can detach event listeners - document.removeEventListener("DOMContentLoaded", onDOMContentLoaded); - // 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 - [ - 'addDocumentStylesToAllIFrames', - 'addDocumentStylesToIFrame', - 'addStyleElement', - 'addStyleToIFrameSrcDoc', - 'applyOnMessage', - 'applySections', - 'applyStyles', - 'disableAll', - 'getDynamicIFrames', - 'iframeIsDynamic', - 'iframeIsLoadingSrcDoc', - 'initObserver', - 'removeStyle', - 'replaceAll', - 'requestStyles', - 'retireStyle' - ].forEach(fn => window[fn] = null); - - // we can destroy global variables - g_styleElements = iframeObserver = retiredStyleIds = null; - } + +function orphanCheck() { + const port = chrome.runtime.connect(); + if (port) { + port.disconnect(); + return; + } + + // we're orphaned due to an extension update + // we can detach the mutation observer + if (docRewriteObserver) { + docRewriteObserver.disconnect(); + } + // we can detach event listeners + window.removeEventListener(chrome.runtime.id, orphanCheck, true); + // we can't detach chrome.runtime.onMessage because it's no longer connected internally + // we can destroy our globals in this context to free up memory + [ // functions + 'addStyleElement', + 'applyOnMessage', + 'applySections', + 'applyStyles', + 'applyStyleState', + 'doDisableAll', + 'initDocRewriteObserver', + 'orphanCheck', + 'removeStyle', + 'replaceAll', + 'requestStyles', + // variables + 'ROOT', + 'disabledElements', + 'retiredStyleTimers', + 'styleElements', + 'docRewriteObserver', + ].forEach(fn => (window[fn] = null)); } diff --git a/background.js b/background.js index bea7653b..2f70e159 100644 --- a/background.js +++ b/background.js @@ -1,236 +1,282 @@ -/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */ +/* global dbExec, getStyles, saveStyle */ +'use strict'; -var frameIdMessageable; -runTryCatch(function() { - chrome.tabs.sendMessage(0, {}, {frameId: 0}, function() { - var clearError = chrome.runtime.lastError; - frameIdMessageable = true; - }); +// eslint-disable-next-line no-var +var browserCommands, contextMenus; + +// ************************************************************************* +// preload the DB and report errors +dbExec().catch((...args) => { + args.forEach(arg => 'message' in arg && console.error(arg.message)); }); -// This happens right away, sometimes so fast that the content script isn't even ready. That's -// why the content script also asks for this stuff. -chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, "styleApply")); -// Not supported in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1239349 -if ("onHistoryStateUpdated" in chrome.webNavigation) { - chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, "styleReplaceAll")); -} -chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null)); -function webNavigationListener(method, data) { - // Until Chrome 41, we can't target a frame with a message - // (https://developer.chrome.com/extensions/tabs#method-sendMessage) - // so a style affecting a page with an iframe will affect the main page as well. - // Skip doing this for frames in pre-41 to prevent page flicker. - if (data.frameId != 0 && !frameIdMessageable) { - return; - } - getStyles({matchUrl: data.url, enabled: true, asHash: true}, function(styleHash) { - if (method) { - chrome.tabs.sendMessage(data.tabId, {method: method, styles: styleHash}, - frameIdMessageable ? {frameId: data.frameId} : undefined); - } - if (data.frameId == 0) { - updateIcon({id: data.tabId, url: data.url}, styleHash); - } - }); +// ************************************************************************* +// register all listeners +chrome.runtime.onMessage.addListener(onRuntimeMessage); + +chrome.webNavigation.onBeforeNavigate.addListener(data => + webNavigationListener(null, data)); + +chrome.webNavigation.onCommitted.addListener(data => + webNavigationListener('styleApply', data)); + +chrome.webNavigation.onHistoryStateUpdated.addListener(data => + webNavigationListener('styleReplaceAll', data)); + +chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => + webNavigationListener('styleReplaceAll', data)); + +chrome.tabs.onAttached.addListener((tabId, data) => { + // When an edit page gets attached or detached, remember its state + // so we can do the same to the next one to open. + chrome.tabs.get(tabId, tab => { + if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) { + chrome.windows.get(tab.windowId, {populate: true}, win => { + // If there's only one tab in this window, it's been dragged to new window + prefs.set('openEditInWindow', win.tabs.length == 1); + }); + } + }); +}); + +chrome.contextMenus.onClicked.addListener((info, tab) => + contextMenus[info.menuItemId].click(info, tab)); + +if ('commands' in chrome) { + // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 + chrome.commands.onCommand.addListener(command => browserCommands[command]()); } -// catch direct URL hash modifications not invoked via HTML5 history API -var tabUrlHasHash = {}; -chrome.tabs.onUpdated.addListener(function(tabId, info, tab) { - if (info.status == "loading" && info.url) { - if (info.url.indexOf('#') > 0) { - tabUrlHasHash[tabId] = true; - } else if (tabUrlHasHash[tabId]) { - delete tabUrlHasHash[tabId]; - } else { - // do nothing since the tab neither had # before nor has # now - return; - } - webNavigationListener("styleReplaceAll", {tabId: tabId, frameId: 0, url: info.url}); - } -}); -chrome.tabs.onRemoved.addListener(function(tabId, info) { - delete tabUrlHasHash[tabId]; -}); - -chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { - switch (request.method) { - case "getStyles": - var styles = getStyles(request, sendResponse); - // check if this is a main content frame style enumeration - if (request.matchUrl && !request.id - && sender && sender.tab && sender.frameId == 0 - && sender.tab.url == request.matchUrl) { - updateIcon(sender.tab, styles); - } - return KEEP_CHANNEL_OPEN; - case "saveStyle": - saveStyle(request).then(sendResponse); - return KEEP_CHANNEL_OPEN; - case "invalidateCache": - if (typeof invalidateCache != "undefined") { - invalidateCache(false); - } - break; - case "healthCheck": - getDatabase(function() { sendResponse(true); }, function() { sendResponse(false); }); - return KEEP_CHANNEL_OPEN; - case "openURL": - openURL(request); - break; - case "styleDisableAll": - chrome.contextMenus.update("disableAll", {checked: request.disableAll}); - break; - case "prefChanged": - if (request.prefName == "show-badge") { - chrome.contextMenus.update("show-badge", {checked: request.value}); - } - else if (request.prefName === 'disableAll') { - chrome.contextMenus.update("disableAll", {checked: request.value}); - } - break; - case "refreshAllTabs": - refreshAllTabs().then(sendResponse); - return KEEP_CHANNEL_OPEN; - } -}); - - -// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 -if ("commands" in chrome) { - chrome.commands.onCommand.addListener(function(command) { - switch (command) { - case "openManage": - openURL({url: chrome.extension.getURL("manage.html")}); - break; - case "styleDisableAll": - disableAllStylesToggle(); - chrome.contextMenus.update("disableAll", {checked: prefs.get("disableAll")}); - break; - } - }); +// ************************************************************************* +{ + const onInstall = ({reason}) => { + chrome.runtime.onInstalled.removeListener(onInstall); + const manifest = chrome.runtime.getManifest(); + // Open FAQs page once after installation to guide new users. + // Do not display it in development mode. + if (reason == 'install' && manifest.update_url) { + setTimeout(openURL, 100, { + url: `http://add0n.com/stylus.html?version=${manifest.version}&type=install` + }); + } + // reset L10N cache on UI language change or update + const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {}; + const UIlang = chrome.i18n.getUILanguage(); + if (reason == 'update' || browserUIlanguage != UIlang) { + localStorage.L10N = JSON.stringify({ + browserUIlanguage: UIlang, + }); + } + }; + // bind for 60 seconds max and auto-unbind if it's a normal run + chrome.runtime.onInstalled.addListener(onInstall); + setTimeout(onInstall, 60e3, {reason: 'unbindme'}); } -// contextMenus API is present in ancient Chrome but it throws an exception -// upon encountering the unsupported parameter value "browser_action", so we have to catch it. -runTryCatch(function() { - chrome.contextMenus.create({ - id: "show-badge", title: chrome.i18n.getMessage("menuShowBadge"), - type: "checkbox", contexts: ["browser_action"], checked: prefs.get("show-badge") - }, function() { var clearError = chrome.runtime.lastError }); - chrome.contextMenus.create({ - id: "disableAll", title: chrome.i18n.getMessage("disableAllStyles"), - type: "checkbox", contexts: ["browser_action"], checked: prefs.get("disableAll") - }, function() { var clearError = chrome.runtime.lastError }); - chrome.contextMenus.create({ - id: "open-manager", title: chrome.i18n.getMessage("openStylesManager"), - type: "normal", contexts: ["browser_action"] - }, function() {var clearError = chrome.runtime.lastError}); +// ************************************************************************* +// browser commands +browserCommands = { + openManage() { + openURL({url: '/manage.html'}); + }, + styleDisableAll(info) { + prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); + }, +}; + +// ************************************************************************* +// context menus +contextMenus = Object.assign({ + 'show-badge': { + title: 'menuShowBadge', + click: info => prefs.set(info.menuItemId, info.checked), + }, + 'disableAll': { + title: 'disableAllStyles', + click: browserCommands.styleDisableAll, + }, + 'open-manager': { + title: 'openStylesManager', + click: browserCommands.openManage, + }, +}, prefs.get('editor.contextDelete') && { + 'editor.contextDelete': { + title: 'editDeleteText', + type: 'normal', + contexts: ['editable'], + documentUrlPatterns: [URLS.ownOrigin + 'edit*'], + click: (info, tab) => { + chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'}); + }, + } }); -chrome.contextMenus.onClicked.addListener(function(info, tab) { - if (info.menuItemId == "disableAll") { - disableAllStylesToggle(info.checked); - } - else if (info.menuItemId === 'show-badge') { - prefs.set(info.menuItemId, info.checked); - } - else if (info.menuItemId === 'open-manager') { - openURL({url: chrome.extension.getURL("manage.html")}); - } -}); - -function disableAllStylesToggle(newState) { - if (newState === undefined || newState === null) { - newState = !prefs.get("disableAll"); - } - prefs.set("disableAll", newState); +{ + const createContextMenus = (ids = Object.keys(contextMenus)) => { + for (const id of ids) { + const item = Object.assign({id}, contextMenus[id]); + const prefValue = prefs.readOnlyValues[id]; + item.title = chrome.i18n.getMessage(item.title); + if (!item.type && typeof prefValue == 'boolean') { + item.type = 'checkbox'; + item.checked = prefValue; + } + if (!item.contexts) { + item.contexts = ['browser_action']; + } + delete item.click; + chrome.contextMenus.create(item, ignoreChromeError); + } + }; + createContextMenus(); + prefs.subscribe((id, checked) => { + if (id == 'editor.contextDelete') { + if (checked) { + createContextMenus([id]); + } else { + chrome.contextMenus.remove(id, ignoreChromeError); + } + } else { + chrome.contextMenus.update(id, {checked}, ignoreChromeError); + } + }, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean')); } -// Get the DB so that any first run actions will be performed immediately when the background page loads. -getDatabase(function() {}, reportError); +// ************************************************************************* +// [re]inject content scripts +{ + const NTP = 'chrome://newtab/'; + const PING = {method: 'ping'}; + const ALL_URLS = ''; + const contentScripts = chrome.runtime.getManifest().content_scripts; + // expand * as .*? + const wildcardAsRegExp = (s, flags) => new RegExp( + s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') + .replace(/\*/g, '.*?'), flags); + for (const cs of contentScripts) { + cs.matches = cs.matches.map(m => ( + m == ALL_URLS ? m : wildcardAsRegExp(m) + )); + } -// When an edit page gets attached or detached, remember its state so we can do the same to the next one to open. -var editFullUrl = chrome.extension.getURL("edit.html"); -chrome.tabs.onAttached.addListener(function(tabId, data) { - chrome.tabs.get(tabId, function(tabData) { - if (tabData.url.indexOf(editFullUrl) == 0) { - chrome.windows.get(tabData.windowId, {populate: true}, function(win) { - // If there's only one tab in this window, it's been dragged to new window - prefs.set("openEditInWindow", win.tabs.length == 1); - }); - } - }); -}); + const injectCS = (cs, tabId) => { + chrome.tabs.executeScript(tabId, { + file: cs.js[0], + runAt: cs.run_at, + allFrames: cs.all_frames, + matchAboutBlank: cs.match_about_blank, + }, ignoreChromeError); + }; -function openURL(options) { - chrome.tabs.query({currentWindow: true, url: options.url}, function(tabs) { - // switch to an existing tab with the requested url - if (tabs.length) { - chrome.tabs.highlight({windowId: tabs[0].windowId, tabs: tabs[0].index}, function (window) {}); - } else { - delete options.method; - getActiveTab(function(tab) { - // re-use an active new tab page - chrome.tabs[tab.url == "chrome://newtab/" ? "update" : "create"](options); - }); - } - }); + const pingCS = (cs, {id, url}) => { + cs.matches.some(match => { + if ((match == ALL_URLS || url.match(match)) + && (!url.startsWith('chrome') || url == NTP)) { + chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id)); + return true; + } + }); + }; + + chrome.tabs.query({}, tabs => + tabs.forEach(tab => + contentScripts.forEach(cs => + pingCS(cs, tab)))); } -var codeMirrorThemes; -getCodeMirrorThemes(function(themes) { - codeMirrorThemes = themes; -}); -// do not use prefs.get('version', null) as it might not yet be available -chrome.storage.local.get('version', prefs => { - // Open FAQs page once after installation to guide new users, - // https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160 - if (!prefs.version) { - // do not display the FAQs page in development mode - if ('update_url' in chrome.runtime.getManifest()) { - let version = chrome.runtime.getManifest().version; - chrome.storage.local.set({ - version - }, () => { - window.setTimeout(() => { - chrome.tabs.create({ - url: 'http://add0n.com/stylus.html?version=' + version + '&type=install' - }); - }, 3000); - }) - } - } -}); +// ************************************************************************* -injectContentScripts(); - -function injectContentScripts() { - const contentScripts = chrome.app.getDetails().content_scripts; - for (let cs of contentScripts) { - cs.matches = cs.matches.map(m => m == '' ? m : wildcardAsRegExp(m)); - } - chrome.tabs.query({url: '*://*/*'}, tabs => { - for (let tab of tabs) { - for (let cs of contentScripts) { - for (let m of cs.matches) { - if (m == '' || tab.url.match(m)) { - chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { - if (!pong) { - chrome.tabs.executeScript(tab.id, { - file: cs.js[0], - runAt: cs.run_at, - allFrames: cs.all_frames, - }, result => chrome.runtime.lastError); // ignore lastError just in case - } - }); - // inject the content script just once - break; - } - } - } - } - }); +function webNavigationListener(method, {url, tabId, frameId}) { + getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => { + if (method && !url.startsWith('chrome:') && tabId >= 0) { + chrome.tabs.sendMessage(tabId, { + method, + // ping own page so it retrieves the styles directly + styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles, + }, { + frameId + }); + } + // main page frame id is 0 + if (frameId == 0) { + updateIcon({id: tabId, url}, styles); + } + }); +} + + +function updateIcon(tab, styles) { + if (tab.id < 0) { + return; + } + if (styles) { + stylesReceived(styles); + return; + } + getTabRealURL(tab) + .then(url => getStyles({matchUrl: url, enabled: true, asHash: true})) + .then(stylesReceived); + + function stylesReceived(styles) { + let numStyles = styles.length; + if (numStyles === undefined) { + // for 'styles' asHash:true fake the length by counting numeric ids manually + numStyles = 0; + for (const id of Object.keys(styles)) { + numStyles += id.match(/^\d+$/) ? 1 : 0; + } + } + const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); + const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; + const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); + const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; + chrome.browserAction.setIcon({ + tabId: tab.id, + path: { + // Material Design 2016 new size is 16px + 16: `images/icon/16${postfix}.png`, + 32: `images/icon/32${postfix}.png`, + // Chromium forks or non-chromium browsers may still use the traditional 19px + 19: `images/icon/19${postfix}.png`, + 38: `images/icon/38${postfix}.png`, + // TODO: add Edge preferred sizes: 20, 25, 30, 40 + }, + }, () => { + if (chrome.runtime.lastError) { + return; + } + // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor + chrome.browserAction.setBadgeBackgroundColor({color}); + getTab(tab.id).then(() => { + chrome.browserAction.setBadgeText({text, tabId: tab.id}); + }); + }); + } +} + + +function onRuntimeMessage(request, sender, sendResponse) { + switch (request.method) { + + case 'getStyles': + getStyles(request).then(sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'saveStyle': + saveStyle(request).then(sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'healthCheck': + dbExec() + .then(() => sendResponse(true)) + .catch(() => sendResponse(false)); + return KEEP_CHANNEL_OPEN; + + case 'download': + download(request.url) + .then(sendResponse) + .catch(() => sendResponse(null)); + return KEEP_CHANNEL_OPEN; + } } diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 2c4736a7..203ff095 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -1,9 +1,9 @@ -/* globals getStyles, saveStyle, invalidateCache, refreshAllTabs, handleUpdate */ +/* global messageBox, handleUpdate, applyOnMessage */ 'use strict'; -var STYLISH_DUMP_FILE_EXT = '.txt'; -var STYLISH_DUMPFILE_EXTENSION = '.json'; -var STYLISH_DEFAULT_SAVE_NAME = 'stylus-mm-dd-yyyy' + STYLISH_DUMP_FILE_EXT; +const STYLISH_DUMP_FILE_EXT = '.txt'; +const STYLUS_BACKUP_FILE_EXT = '.json'; + function importFromFile({fileTypeFilter, file} = {}) { return new Promise(resolve => { @@ -25,7 +25,7 @@ function importFromFile({fileTypeFilter, file} = {}) { function readFile() { if (file || fileInput.value !== fileInput.initialValue) { file = file || fileInput.files[0]; - if (file.size > 100*1000*1000) { + if (file.size > 100e6) { console.warn("100MB backup? I don't believe you."); importFromString('').then(resolve); return; @@ -45,103 +45,312 @@ function importFromFile({fileTypeFilter, file} = {}) { }); } + function importFromString(jsonString) { - const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || []; - const numStyles = json.length; - - if (numStyles) { - invalidateCache(true); + if (!BG) { + onBackgroundReady().then(() => importFromString(jsonString)); + return; } + // create objects in background context + const json = BG.tryJSONparse(jsonString) || []; + if (typeof json.slice != 'function') { + json.length = 0; + } + const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); + const oldStylesByName = json.length && new Map( + oldStyles.map(style => [style.name.trim(), style])); - return new Promise(resolve => { - proceed(); - function proceed() { - const nextStyle = json.shift(); - if (nextStyle) { - saveStyle(nextStyle, {notify: false}).then(style => { - handleUpdate(style); - setTimeout(proceed, 0); - }); - } else { - refreshAllTabs().then(() => { - setTimeout(alert, 100, numStyles + ' styles installed/updated'); - resolve(numStyles); - }); + let oldDigests; + chrome.storage.local.get(null, data => (oldDigests = data)); + + const stats = { + added: {names: [], ids: [], legend: 'importReportLegendAdded'}, + unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, + metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, + metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, + codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, + invalid: {names: [], legend: 'importReportLegendInvalid'}, + }; + + let index = 0; + let lastRenderTime = performance.now(); + const renderQueue = []; + const RENDER_NAP_TIME_MAX = 1000; // ms + const RENDER_QUEUE_MAX = 50; // number of styles + const SAVE_OPTIONS = {reason: 'import', notify: false}; + + return new Promise(proceed); + + function proceed(resolve) { + while (index < json.length) { + const item = json[index++]; + const info = analyze(item); + if (info) { + // using saveStyle directly since json was parsed in background page context + return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) + .then(style => account({style, info, resolve})); } } - }); + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + renderQueue.length = 0; + done(resolve); + } + + function analyze(item) { + if (!item || !item.name || !item.name.trim() || typeof item != 'object' + || (item.sections && typeof item.sections.slice != 'function')) { + stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); + return; + } + item.name = item.name.trim(); + const byId = BG.cachedStyles.byId.get(item.id); + const byName = oldStylesByName.get(item.name); + const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName; + if (oldStyle == byName && byName) { + item.id = byName.id; + } + const oldStyleKeys = oldStyle && Object.keys(oldStyle); + const metaEqual = oldStyleKeys && + oldStyleKeys.length == Object.keys(item).length && + oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]); + const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); + if (metaEqual && codeEqual) { + stats.unchanged.names.push(oldStyle.name); + stats.unchanged.ids.push(oldStyle.id); + return; + } + return {oldStyle, metaEqual, codeEqual}; + } + + function account({style, info, resolve}) { + renderQueue.push(style); + if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX + || renderQueue.length > RENDER_QUEUE_MAX) { + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); + renderQueue.length = 0; + lastRenderTime = performance.now(); + } + setTimeout(proceed, 0, resolve); + const {oldStyle, metaEqual, codeEqual} = info; + if (!oldStyle) { + stats.added.names.push(style.name); + stats.added.ids.push(style.id); + return; + } + if (!metaEqual && !codeEqual) { + stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); + stats.metaAndCode.ids.push(style.id); + return; + } + if (!codeEqual) { + stats.codeOnly.names.push(style.name); + stats.codeOnly.ids.push(style.id); + return; + } + stats.metaOnly.names.push(reportNameChange(oldStyle, style)); + stats.metaOnly.ids.push(style.id); + } + + function done(resolve) { + const numChanged = stats.metaAndCode.names.length + + stats.metaOnly.names.length + + stats.codeOnly.names.length + + stats.added.names.length; + Promise.resolve(numChanged && refreshAllTabs()).then(() => { + const report = Object.keys(stats) + .filter(kind => stats[kind].names.length) + .map(kind => { + const {ids, names, legend} = stats[kind]; + const listItemsWithId = (name, i) => + $element({dataset: {id: ids[i]}, textContent: name}); + const listItems = name => + $element({textContent: name}); + const block = + $element({tag: 'details', dataset: {id: kind}, appendChild: [ + $element({tag: 'summary', appendChild: + $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) + }), + $element({tag: 'small', appendChild: + names.map(ids ? listItemsWithId : listItems) + }), + ]}); + return block; + }); + scrollTo(0, 0); + messageBox({ + title: t('importReportTitle'), + contents: report.length ? report : t('importReportUnchanged'), + buttons: [t('confirmOK'), numChanged && t('undo')], + onshow: bindClick, + }).then(({button, enter, esc}) => { + if (button == 1) { + undo(); + } + }); + resolve(numChanged); + }); + } + + function undo() { + const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); + const newIds = [ + ...stats.metaAndCode.ids, + ...stats.metaOnly.ids, + ...stats.codeOnly.ids, + ...stats.added.ids, + ]; + let resolve; + index = 0; + return new Promise(resolve_ => { + resolve = resolve_; + undoNextId(); + }).then(BG.refreshAllTabs) + .then(() => messageBox({ + title: t('importReportUndoneTitle'), + contents: newIds.length + ' ' + t('importReportUndone'), + buttons: [t('confirmOK')], + })); + function undoNextId() { + if (index == newIds.length) { + resolve(); + return; + } + const id = newIds[index++]; + deleteStyleSafe({id, notify: false}).then(id => { + const oldStyle = oldStylesById.get(id); + if (oldStyle) { + oldStyle.styleDigest = oldDigests[BG.DIGEST_KEY_PREFIX + id]; + saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) + .then(undoNextId); + } else { + undoNextId(); + } + }); + } + } + + function bindClick(box) { + const highlightElement = event => { + const styleElement = $('#style-' + event.target.dataset.id); + if (styleElement) { + scrollElementIntoView(styleElement); + animateElement(styleElement, {className: 'highlight'}); + } + }; + for (const block of $$('details')) { + if (block.dataset.id != 'invalid') { + block.style.cursor = 'pointer'; + block.onclick = highlightElement; + } + } + } + + function limitString(s, limit = 100) { + return s.length <= limit ? s : s.substr(0, limit) + '...'; + } + + function reportNameChange(oldStyle, newStyle) { + return newStyle.name != oldStyle.name + ? oldStyle.name + ' —> ' + newStyle.name + : oldStyle.name; + } + + function refreshAllTabs() { + return getActiveTab().then(activeTab => new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + chrome.tabs.query({}, tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.id == activeTab.id) { + applyOnMessage(message); + } else { + chrome.tabs.sendMessage(tab.id, message); + } + BG.updateIcon(tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + })); + } } -function generateFileName() { - var today = new Date(); - var dd = '0' + today.getDate(); - var mm = '0' + (today.getMonth() + 1); - var yyyy = today.getFullYear(); - dd = dd.substr(-2); - mm = mm.substr(-2); - - today = mm + '-' + dd + '-' + yyyy; - - return 'stylus-' + today + STYLISH_DUMPFILE_EXTENSION; -} - -document.getElementById('file-all-styles').onclick = () => { - getStyles({}, function (styles) { - let text = JSON.stringify(styles, null, '\t'); - let fileName = generateFileName() || STYLISH_DEFAULT_SAVE_NAME; - - let url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); +$('#file-all-styles').onclick = () => { + Promise.all([ + BG.chromeLocal.get(null), + getStylesSafe(), + ]).then(([data, styles]) => { + styles = styles.map(style => { + const styleDigest = data[BG.DIGEST_KEY_PREFIX + style.id]; + return styleDigest ? Object.assign({styleDigest}, style) : style; + }); + const text = JSON.stringify(styles, null, '\t'); + const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); + return url; // for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600 - fetch(url) + }).then(fetch) .then(res => res.blob()) .then(blob => { - let a = document.createElement('a'); - a.setAttribute('download', fileName); - a.setAttribute('href', URL.createObjectURL(blob)); - a.dispatchEvent(new MouseEvent('click')); + const objectURL = URL.createObjectURL(blob); + Object.assign(document.createElement('a'), { + download: generateFileName(), + href: objectURL, + type: 'application/json', + }).dispatchEvent(new MouseEvent('click')); + setTimeout(() => URL.revokeObjectURL(objectURL)); }); - }); + + function generateFileName() { + const today = new Date(); + const dd = ('0' + today.getDate()).substr(-2); + const mm = ('0' + (today.getMonth() + 1)).substr(-2); + const yyyy = today.getFullYear(); + return `stylus-${mm}-${dd}-${yyyy}${STYLUS_BACKUP_FILE_EXT}`; + } }; -document.getElementById('unfile-all-styles').onclick = () => { - importFromFile({fileTypeFilter: STYLISH_DUMPFILE_EXTENSION}); +$('#unfile-all-styles').onclick = () => { + importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); }; -const dropTarget = Object.assign(document.body, { - ondragover: event => { +Object.assign(document.body, { + ondragover(event) { const hasFiles = event.dataTransfer.types.includes('Files'); event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none'; - dropTarget.classList.toggle('dropzone', hasFiles); + this.classList.toggle('dropzone', hasFiles); if (hasFiles) { event.preventDefault(); - clearTimeout(dropTarget.fadeoutTimer); - dropTarget.classList.remove('fadeout'); + clearTimeout(this.fadeoutTimer); + this.classList.remove('fadeout'); } }, - ondragend: event => { - dropTarget.classList.add('fadeout'); - // transitionend event may not fire if the user switched to another tab so we'll use a timer - clearTimeout(dropTarget.fadeoutTimer); - dropTarget.fadeoutTimer = setTimeout(() => { - dropTarget.classList.remove('dropzone', 'fadeout'); - }, 250); + ondragend(event) { + animateElement(this, {className: 'fadeout'}).then(() => { + this.style.animationDuration = ''; + this.classList.remove('dropzone'); + }); }, - ondragleave: event => { + ondragleave(event) { // Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds if (!event.screenX && !event.screenX) { - dropTarget.ondragend(); + this.ondragend(); } }, - ondrop: event => { + ondrop(event) { + this.ondragend(); if (event.dataTransfer.files.length) { event.preventDefault(); - importFromFile({file: event.dataTransfer.files[0]}).then(() => { - dropTarget.classList.remove('dropzone'); - }); - } else { - dropTarget.ondragend(); + if ($('#onlyUpdates input').checked) { + $('#onlyUpdates input').click(); + } + importFromFile({file: event.dataTransfer.files[0]}); } }, }); diff --git a/beautify/beautify-css-mod.js b/beautify/beautify-css-mod.js new file mode 100644 index 00000000..d23ea125 --- /dev/null +++ b/beautify/beautify-css-mod.js @@ -0,0 +1,655 @@ +/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */ +/* + + The MIT License (MIT) + + Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + + CSS Beautifier +--------------- + + Written by Harutyun Amirjanyan, (amirjanyan@gmail.com) + + Based on code initially developed by: Einar Lielmanis, + http://jsbeautifier.org/ + + Usage: + css_beautify(source_text); + css_beautify(source_text, options); + + The options are (default in brackets): + indent_size (4) — indentation size, + indent_char (space) — character to indent with, + preserve_newlines (default false) - whether existing line breaks should be preserved, + selector_separator_newline (true) - separate selectors with newline or + not (e.g. "a,\nbr" or "a, br") + end_with_newline (false) - end with a newline + newline_between_rules (true) - add a new line after every css rule + space_around_selector_separator (false) - ensure space around selector separators: + '>', '+', '~' (e.g. "a>b" -> "a > b") + e.g + + css_beautify(css_source_text, { + 'indent_size': 1, + 'indent_char': '\t', + 'selector_separator': ' ', + 'end_with_newline': false, + 'newline_between_rules': true, + 'space_around_selector_separator': true + }); +*/ + +// http://www.w3.org/TR/CSS21/syndata.html#tokenization +// http://www.w3.org/TR/css3-syntax/ + +(function() { + + function mergeOpts(allOptions, targetType) { + var finalOpts = {}; + var name; + + for (name in allOptions) { + if (name !== targetType) { + finalOpts[name] = allOptions[name]; + } + } + + + //merge in the per type settings for the targetType + if (targetType in allOptions) { + for (name in allOptions[targetType]) { + finalOpts[name] = allOptions[targetType][name]; + } + } + return finalOpts; + } + + var lineBreak = /\r\n|[\n\r\u2028\u2029]/; + var allLineBreaks = new RegExp(lineBreak.source, 'g'); + + function css_beautify(source_text, options) { + options = options || {}; + + // Allow the setting of language/file-type specific options + // with inheritance of overall settings + options = mergeOpts(options, 'css'); + + source_text = source_text || ''; + + var newlinesFromLastWSEat = 0; + var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4; + var indentCharacter = options.indent_char || ' '; + var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines; + var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline; + var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline; + var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules; + var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator; + space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator); + var eol = options.eol ? options.eol : 'auto'; + + /* STYLUS: hack start */ + const defaultOption = (opt, defaultValue) => opt === undefined ? defaultValue : opt; + var newline_between_properties = defaultOption(options.newline_between_properties, true); + var newline_before_open_brace = defaultOption(options.newline_before_open_brace, false); + var newline_after_open_brace = defaultOption(options.newline_after_open_brace, true); + var newline_before_close_brace = defaultOption(options.newline_before_close_brace, true); + var translatePos = (options.translate_positions || [])[0]; + var translatePosIndex = 0; + var translatePosLine = translatePos && translatePos.line; + var translatePosCol = translatePos && translatePos.ch; + var inputPosLine = 0, inputPosCol = 0; + var outputPosLine = 0, outputPosCol = 0; + /* STYLUS: hack end */ + + if (options.indent_with_tabs) { + indentCharacter = '\t'; + indentSize = 1; + } + + if (eol === 'auto') { + eol = '\n'; + if (source_text && lineBreak.test(source_text || '')) { + eol = source_text.match(lineBreak)[0]; + } + } + + eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n'); + + // HACK: newline parsing inconsistent. This brute force normalizes the input. + source_text = source_text.replace(allLineBreaks, '\n'); + + // tokenizer + var whiteRe = /^\s+$/; + + var pos = -1, + ch; + var parenLevel = 0; + + function next(resetLine, resetCol) { + if (resetLine !== undefined) { + inputPosLine = resetLine; + inputPosCol = resetCol; + if (inputPosCol < 0) { + inputPosLine--; + inputPosCol = pos - source_text.lastIndexOf('\n', pos); + } + } + ch = source_text.charAt(++pos); + if (translatePos) { + inputPosCol++; + if (ch == '\n') { + inputPosLine++; + inputPosCol = 0; + } + if (inputPosLine == translatePosLine && inputPosCol >= translatePosCol + || inputPosLine > translatePosLine) { + translatePos.line = outputPosLine - (inputPosLine - translatePosLine); + translatePos.ch = outputPosCol - (inputPosCol - translatePosCol); + translatePos.ch += translatePos.ch ? 1 : 0; + translatePos = options.translate_positions[++translatePosIndex]; + translatePosLine = translatePos && translatePos.line; + translatePosCol = translatePos && translatePos.ch; + } + } + return ch || ''; + } + + function peek(skipWhitespace) { + var result = ''; + var prev_pos = pos; + var prevInputPosLine = inputPosLine; + var prevInputPosCol = inputPosCol; + if (skipWhitespace) { + eatWhitespace(); + } + result = source_text.charAt(pos + 1) || ''; + pos = prev_pos - 1; + next(prevInputPosLine, prevInputPosCol - 1); + return result; + } + + function eatString(endChars) { + var start = pos; + while (next()) { + if (ch === "\\") { + next(); + } else if (endChars.indexOf(ch) !== -1) { + break; + } else if (ch === "\n") { + break; + } + } + return source_text.substring(start, pos + 1); + } + + function peekString(endChar) { + var prev_pos = pos; + var prevInputPosLine = inputPosLine; + var prevInputPosCol = inputPosCol; + var str = eatString(endChar); + pos = prev_pos - 1; + next(prevInputPosLine, prevInputPosCol - 1); + return str; + } + + function eatWhitespace(preserve_newlines_local) { + var result = 0; + while (whiteRe.test(peek())) { + next(); + if (ch === '\n' && preserve_newlines_local && preserve_newlines) { + print.newLine(true); + result++; + } + } + newlinesFromLastWSEat = result; + return result; + } + + function skipWhitespace() { + var result = ''; + if (ch && whiteRe.test(ch)) { + result = ch; + } + while (whiteRe.test(next())) { + result += ch; + } + return result; + } + + function eatComment(singleLine) { + var start = pos; + singleLine = peek() === "/"; + next(); + while (next()) { + if (!singleLine && ch === "*" && peek() === "/") { + next(); + break; + } else if (singleLine && ch === "\n") { + return source_text.substring(start, pos); + } + } + + return source_text.substring(start, pos) + ch; + } + + + function lookBack(str) { + return source_text.substring(pos - str.length, pos).toLowerCase() === + str; + } + + // Nested pseudo-class if we are insideRule + // and the next special character found opens + // a new block + function foundNestedPseudoClass() { + var openParen = 0; + for (var i = pos + 1; i < source_text.length; i++) { + var ch = source_text.charAt(i); + if (ch === "{") { + return true; + } else if (ch === '(') { + // pseudoclasses can contain () + openParen += 1; + } else if (ch === ')') { + if (openParen === 0) { + return false; + } + openParen -= 1; + } else if (ch === ";" || ch === "}") { + return false; + } + } + return false; + } + + // printer + var basebaseIndentString = source_text.match(/^[\t ]*/)[0]; + var singleIndent = new Array(indentSize + 1).join(indentCharacter); + var indentLevel = 0; + var nestedLevel = 0; + + function indent() { + indentLevel++; + basebaseIndentString += singleIndent; + } + + function outdent() { + indentLevel--; + basebaseIndentString = basebaseIndentString.slice(0, -indentSize); + } + + var print = {}; + print["{"] = function(ch) { + newline_before_open_brace ? print.newLine() : print.singleSpace(); + output.push(ch); + outputPosCol++; + if (!eatWhitespace(true)) { + newline_after_open_brace ? print.newLine() : print.singleSpace(); + } + }; + print["}"] = function(newline) { + if (newline) { + newline_before_close_brace ? print.newLine() : (print.trim(), print.singleSpace()); + } + output.push('}'); + outputPosCol++; + if (!eatWhitespace(true)) { + print.newLine(); + } + }; + + print._lastCharWhitespace = function() { + return whiteRe.test(output[output.length - 1]); + }; + + print.newLine = function(keepWhitespace) { + if (output.length) { + if (!keepWhitespace && output[output.length - 1] !== '\n') { + print.trim(); + } else if (output[output.length - 1] === basebaseIndentString) { + output.pop(); + outputPosCol -= basebaseIndentString.length; + } + output.push('\n'); + outputPosLine++; + outputPosCol = 0; + + if (basebaseIndentString) { + output.push(basebaseIndentString); + outputPosCol += basebaseIndentString.length; + } + } + }; + print.singleSpace = function() { + if (output.length && !print._lastCharWhitespace()) { + output.push(' '); + outputPosCol++; + } + }; + + print.preserveSingleSpace = function() { + if (isAfterSpace) { + print.singleSpace(); + } + }; + + print.trim = function() { + while (print._lastCharWhitespace()) { + const text = output.pop(); + if (text.indexOf('\n') >= 0) { + outputPosLine -= text.match(/\n/g).length; + } + } + outputPosCol = 0; + let i = output.length, token; + while (--i >= 0 && (token = output[i]) != '\n') { + outputPosCol += token.length; + } + }; + + print.text = function(text) { + output.push(text); + if (text.indexOf('\n') < 0) { + outputPosCol += text.length; + } else { + outputPosLine += text.match(/\n/g).length; + outputPosCol = text.length - text.lastIndexOf('\n') - 1; + } + }; + + + var output = []; + /*_____________________--------------------_____________________*/ + + var insideRule = false; + var insidePropertyValue = false; + var enteringConditionalGroup = false; + var top_ch = ''; + var last_top_ch = ''; + + while (true) { + var whitespace = skipWhitespace(); + var isAfterSpace = whitespace !== ''; + var isAfterNewline = whitespace.indexOf('\n') !== -1; + last_top_ch = top_ch; + top_ch = ch; + + if (!ch) { + break; + } else if (ch === '/' && peek() === '*') { /* css comment */ + var header = indentLevel === 0; + + if (isAfterNewline || header) { + print.newLine(); + } + + print.text(eatComment()); + print.newLine(); + if (header) { + print.newLine(true); + } + } else if (ch === '/' && peek() === '/') { // single line comment + if (!isAfterNewline && last_top_ch !== '{') { + print.trim(); + } + print.singleSpace(); + print.text(eatComment()); + print.newLine(); + } else if (ch === '@') { + print.preserveSingleSpace(); + + // deal with less propery mixins @{...} + if (peek() === '{') { + print.text(eatString('}')); + } else { + output.push(ch); + outputPosCol++; + + // strip trailing space, if present, for hash property checks + var variableOrRule = peekString(": ,;{}()[]/='\""); + + if (variableOrRule.match(/[ :]$/)) { + // we have a variable or pseudo-class, add it and insert one space before continuing + next(); + variableOrRule = eatString(": ").replace(/\s$/, ''); + print.text(variableOrRule); + print.singleSpace(); + } + + variableOrRule = variableOrRule.replace(/\s$/, ''); + + // might be a nesting at-rule + if (variableOrRule in css_beautify.NESTED_AT_RULE) { + nestedLevel += 1; + if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) { + enteringConditionalGroup = true; + } + } + } + } else if (ch === '#' && peek() === '{') { + print.preserveSingleSpace(); + print.text(eatString('}')); + } else if (ch === '{') { + if (peek(true) === '}') { + eatWhitespace(); + next(); + print.singleSpace(); + output.push("{"); + outputPosCol++; + print['}'](false); + if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) { + print.newLine(true); + } + } else { + indent(); + print["{"](ch); + // when entering conditional groups, only rulesets are allowed + if (enteringConditionalGroup) { + enteringConditionalGroup = false; + insideRule = (indentLevel > nestedLevel); + } else { + // otherwise, declarations are also allowed + insideRule = (indentLevel >= nestedLevel); + } + } + } else if (ch === '}') { + outdent(); + print["}"](true); + insideRule = false; + insidePropertyValue = false; + if (nestedLevel) { + nestedLevel--; + } + if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) { + print.newLine(true); + } + } else if (ch === ":") { + eatWhitespace(); + if ((insideRule || enteringConditionalGroup) && + !(lookBack("&") || foundNestedPseudoClass()) && + !lookBack("(")) { + // 'property: value' delimiter + // which could be in a conditional group query + output.push(':'); + outputPosCol++; + if (!insidePropertyValue) { + insidePropertyValue = true; + print.singleSpace(); + } + } else { + // sass/less parent reference don't use a space + // sass nested pseudo-class don't use a space + + // preserve space before pseudoclasses/pseudoelements, as it means "in any child" + if (lookBack(" ") && output[output.length - 1] !== " ") { + output.push(" "); + outputPosCol++; + } + if (peek() === ":") { + // pseudo-element + next(); + output.push("::"); + outputPosCol += 2; + } else { + // pseudo-class + output.push(':'); + outputPosCol++; + } + } + } else if (ch === '"' || ch === '\'') { + print.preserveSingleSpace(); + print.text(eatString(ch)); + } else if (ch === ';') { + insidePropertyValue = false; + output.push(ch); + outputPosCol++; + if (!eatWhitespace(true)) { + newline_between_properties ? print.newLine() : print.singleSpace(); + } + } else if (ch === '(') { // may be a url + if (lookBack("url")) { + output.push(ch); + outputPosCol++; + eatWhitespace(); + if (next()) { + if (ch !== ')' && ch !== '"' && ch !== '\'') { + print.text(eatString(')')); + } else { + pos--; + } + } + } else { + parenLevel++; + print.preserveSingleSpace(); + output.push(ch); + outputPosCol++; + eatWhitespace(); + } + } else if (ch === ')') { + output.push(ch); + outputPosCol++; + parenLevel--; + } else if (ch === ',') { + output.push(ch); + outputPosCol++; + if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) { + print.newLine(); + } else { + print.singleSpace(); + } + } else if ((ch === '>' || ch === '+' || ch === '~') && + !insidePropertyValue && parenLevel < 1) { + //handle combinator spacing + if (space_around_combinator) { + print.singleSpace(); + output.push(ch); + outputPosCol++; + print.singleSpace(); + } else { + output.push(ch); + outputPosCol++; + eatWhitespace(); + // squash extra whitespace + if (ch && whiteRe.test(ch)) { + ch = ''; + } + } + } else if (ch === ']') { + output.push(ch); + outputPosCol++; + } else if (ch === '[') { + print.preserveSingleSpace(); + output.push(ch); + outputPosCol++; + } else if (ch === '=') { // no whitespace before or after + eatWhitespace(); + output.push('='); + outputPosCol++; + if (whiteRe.test(ch)) { + ch = ''; + } + } else { + print.preserveSingleSpace(); + output.push(ch); + outputPosCol++; + } + } + + + var sweetCode = ''; + if (basebaseIndentString) { + sweetCode += basebaseIndentString; + } + + sweetCode += output.join('').replace(/[\r\n\t ]+$/, ''); + + // establish end_with_newline + if (end_with_newline) { + sweetCode += '\n'; + } + + if (eol !== '\n') { + sweetCode = sweetCode.replace(/[\n]/g, eol); + } + + return sweetCode; + } + + // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule + css_beautify.NESTED_AT_RULE = { + "@page": true, + "@font-face": true, + "@keyframes": true, + // also in CONDITIONAL_GROUP_RULE below + "@media": true, + "@supports": true, + "@document": true + }; + css_beautify.CONDITIONAL_GROUP_RULE = { + "@media": true, + "@supports": true, + "@document": true + }; + + /*global define */ + if (typeof define === "function" && define.amd) { + // Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- ) + define([], function() { + return { + css_beautify: css_beautify + }; + }); + } else if (typeof exports !== "undefined") { + // Add support for CommonJS. Just put this file somewhere on your require.paths + // and you will be able to `var html_beautify = require("beautify").html_beautify`. + exports.css_beautify = css_beautify; + } else if (typeof window !== "undefined") { + // If we're running a web page and don't have either of the above, add our one global + window.css_beautify = css_beautify; + } else if (typeof global !== "undefined") { + // If we don't even have window, try global. + global.css_beautify = css_beautify; + } + +}()); \ No newline at end of file diff --git a/beautify/beautify-css.js b/beautify/beautify-css.js index c9f021e7..5bfd98b3 100644 --- a/beautify/beautify-css.js +++ b/beautify/beautify-css.js @@ -3,7 +3,7 @@ The MIT License (MIT) - Copyright (c) 2007-2013 Einar Lielmanis and contributors. + Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -39,13 +39,15 @@ css_beautify(source_text, options); The options are (default in brackets): - indent_size (4) — indentation size, - indent_char (space) — character to indent with, - selector_separator_newline (true) - separate selectors with newline or - not (e.g. "a,\nbr" or "a, br") - end_with_newline (false) - end with a newline - newline_between_rules (true) - add a new line after every css rule - + indent_size (4) — indentation size, + indent_char (space) — character to indent with, + preserve_newlines (default false) - whether existing line breaks should be preserved, + selector_separator_newline (true) - separate selectors with newline or + not (e.g. "a,\nbr" or "a, br") + end_with_newline (false) - end with a newline + newline_between_rules (true) - add a new line after every css rule + space_around_selector_separator (false) - ensure space around selector separators: + '>', '+', '~' (e.g. "a>b" -> "a > b") e.g css_beautify(css_source_text, { @@ -53,7 +55,8 @@ 'indent_char': '\t', 'selector_separator': ' ', 'end_with_newline': false, - 'newline_between_rules': true + 'newline_between_rules': true, + 'space_around_selector_separator': true }); */ @@ -61,30 +64,69 @@ // http://www.w3.org/TR/css3-syntax/ (function() { + + function mergeOpts(allOptions, targetType) { + var finalOpts = {}; + var name; + + for (name in allOptions) { + if (name !== targetType) { + finalOpts[name] = allOptions[name]; + } + } + + + //merge in the per type settings for the targetType + if (targetType in allOptions) { + for (name in allOptions[targetType]) { + finalOpts[name] = allOptions[targetType][name]; + } + } + return finalOpts; + } + + var lineBreak = /\r\n|[\n\r\u2028\u2029]/; + var allLineBreaks = new RegExp(lineBreak.source, 'g'); + function css_beautify(source_text, options) { - function defaultOption(opt, defaultValue) { - return opt === undefined ? defaultValue : opt; - } options = options || {}; - var indentSize = options.indent_size || 4; - var indentCharacter = options.indent_char || ' '; - var selectorSeparatorNewline = defaultOption(options.selector_separator_newline, true); - var end_with_newline = defaultOption(options.end_with_newline, false); - var newline_between_rules = defaultOption(options.newline_between_rules, true); - var newline_between_properties = defaultOption(options.newline_between_properties, true); - var newline_before_open_brace = defaultOption(options.newline_before_open_brace, false); - var newline_after_open_brace = defaultOption(options.newline_after_open_brace, true); - var newline_before_close_brace = defaultOption(options.newline_before_close_brace, true); - // compatibility - if (typeof indentSize === "string") { - indentSize = parseInt(indentSize, 10); + // Allow the setting of language/file-type specific options + // with inheritance of overall settings + options = mergeOpts(options, 'css'); + + source_text = source_text || ''; + + var newlinesFromLastWSEat = 0; + var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4; + var indentCharacter = options.indent_char || ' '; + var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines; + var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline; + var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline; + var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules; + var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator; + space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator); + var eol = options.eol ? options.eol : 'auto'; + + if (options.indent_with_tabs) { + indentCharacter = '\t'; + indentSize = 1; } + if (eol === 'auto') { + eol = '\n'; + if (source_text && lineBreak.test(source_text || '')) { + eol = source_text.match(lineBreak)[0]; + } + } + + eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n'); + + // HACK: newline parsing inconsistent. This brute force normalizes the input. + source_text = source_text.replace(allLineBreaks, '\n'); // tokenizer var whiteRe = /^\s+$/; - var wordRe = /[\w$\-_]/; var pos = -1, ch; @@ -96,6 +138,7 @@ } function peek(skipWhitespace) { + var result = ''; var prev_pos = pos; if (skipWhitespace) { eatWhitespace(); @@ -128,12 +171,16 @@ return str; } - function eatWhitespace() { - var result = ''; + function eatWhitespace(preserve_newlines_local) { + var result = 0; while (whiteRe.test(peek())) { next(); - result += ch; + if (ch === '\n' && preserve_newlines_local && preserve_newlines) { + print.newLine(true); + result++; + } } + newlinesFromLastWSEat = result; return result; } @@ -174,11 +221,20 @@ // and the next special character found opens // a new block function foundNestedPseudoClass() { + var openParen = 0; for (var i = pos + 1; i < source_text.length; i++) { var ch = source_text.charAt(i); if (ch === "{") { return true; - } else if (ch === ";" || ch === "}" || ch === ")") { + } else if (ch === '(') { + // pseudoclasses can contain () + openParen += 1; + } else if (ch === ')') { + if (openParen === 0) { + return false; + } + openParen -= 1; + } else if (ch === ";" || ch === "}") { return false; } } @@ -203,14 +259,20 @@ var print = {}; print["{"] = function(ch) { - newline_before_open_brace ? output.push('\n') : print.singleSpace(); + print.singleSpace(); output.push(ch); - newline_after_open_brace ? print.newLine() : print.singleSpace(); + if (!eatWhitespace(true)) { + print.newLine(); + } }; - print["}"] = function(ch) { - newline_before_close_brace ? print.newLine() : print.singleSpace(); - output.push(ch); - print.newLine(); + print["}"] = function(newline) { + if (newline) { + print.newLine(); + } + output.push('}'); + if (!eatWhitespace(true)) { + print.newLine(); + } }; print._lastCharWhitespace = function() { @@ -218,15 +280,17 @@ }; print.newLine = function(keepWhitespace) { - if (!keepWhitespace) { - print.trim(); - } - if (output.length) { + if (!keepWhitespace && output[output.length - 1] !== '\n') { + print.trim(); + } else if (output[output.length - 1] === basebaseIndentString) { + output.pop(); + } output.push('\n'); - } - if (basebaseIndentString) { - output.push(basebaseIndentString); + + if (basebaseIndentString) { + output.push(basebaseIndentString); + } } }; print.singleSpace = function() { @@ -235,6 +299,12 @@ } }; + print.preserveSingleSpace = function() { + if (isAfterSpace) { + print.singleSpace(); + } + }; + print.trim = function() { while (print._lastCharWhitespace()) { output.pop(); @@ -243,12 +313,10 @@ var output = []; - if (basebaseIndentString) { - output.push(basebaseIndentString); - } /*_____________________--------------------_____________________*/ var insideRule = false; + var insidePropertyValue = false; var enteringConditionalGroup = false; var top_ch = ''; var last_top_ch = ''; @@ -263,8 +331,12 @@ if (!ch) { break; } else if (ch === '/' && peek() === '*') { /* css comment */ - var header = lookBack(""); - print.newLine(); + var header = indentLevel === 0; + + if (isAfterNewline || header) { + print.newLine(); + } + output.push(eatComment()); print.newLine(); if (header) { @@ -278,40 +350,46 @@ output.push(eatComment()); print.newLine(); } else if (ch === '@') { - // pass along the space we found as a separate item - if (isAfterSpace) { - print.singleSpace(); - } - output.push(ch); + print.preserveSingleSpace(); - // strip trailing space, if present, for hash property checks - var variableOrRule = peekString(": ,;{}()[]/='\""); + // deal with less propery mixins @{...} + if (peek() === '{') { + output.push(eatString('}')); + } else { + output.push(ch); - if (variableOrRule.match(/[ :]$/)) { - // we have a variable or pseudo-class, add it and insert one space before continuing - next(); - variableOrRule = eatString(": ").replace(/\s$/, ''); - output.push(variableOrRule); - print.singleSpace(); - } + // strip trailing space, if present, for hash property checks + var variableOrRule = peekString(": ,;{}()[]/='\""); - variableOrRule = variableOrRule.replace(/\s$/, '') + if (variableOrRule.match(/[ :]$/)) { + // we have a variable or pseudo-class, add it and insert one space before continuing + next(); + variableOrRule = eatString(": ").replace(/\s$/, ''); + output.push(variableOrRule); + print.singleSpace(); + } - // might be a nesting at-rule - if (variableOrRule in css_beautify.NESTED_AT_RULE) { - nestedLevel += 1; - if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) { - enteringConditionalGroup = true; + variableOrRule = variableOrRule.replace(/\s$/, ''); + + // might be a nesting at-rule + if (variableOrRule in css_beautify.NESTED_AT_RULE) { + nestedLevel += 1; + if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) { + enteringConditionalGroup = true; + } } } + } else if (ch === '#' && peek() === '{') { + print.preserveSingleSpace(); + output.push(eatString('}')); } else if (ch === '{') { if (peek(true) === '}') { eatWhitespace(); next(); print.singleSpace(); - output.push("{}"); - print.newLine(); - if (newline_between_rules && indentLevel === 0) { + output.push("{"); + print['}'](false); + if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) { print.newLine(true); } } else { @@ -328,25 +406,35 @@ } } else if (ch === '}') { outdent(); - print["}"](ch); + print["}"](true); insideRule = false; + insidePropertyValue = false; if (nestedLevel) { nestedLevel--; } - if (newline_between_rules && indentLevel === 0) { + if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) { print.newLine(true); } } else if (ch === ":") { eatWhitespace(); if ((insideRule || enteringConditionalGroup) && - !(lookBack("&") || foundNestedPseudoClass())) { + !(lookBack("&") || foundNestedPseudoClass()) && + !lookBack("(")) { // 'property: value' delimiter // which could be in a conditional group query output.push(':'); - print.singleSpace(); + if (!insidePropertyValue) { + insidePropertyValue = true; + print.singleSpace(); + } } else { // sass/less parent reference don't use a space // sass nested pseudo-class don't use a space + + // preserve space before pseudoclasses/pseudoelements, as it means "in any child" + if (lookBack(" ") && output[output.length - 1] !== " ") { + output.push(" "); + } if (peek() === ":") { // pseudo-element next(); @@ -357,13 +445,14 @@ } } } else if (ch === '"' || ch === '\'') { - if (isAfterSpace) { - print.singleSpace(); - } + print.preserveSingleSpace(); output.push(eatString(ch)); } else if (ch === ';') { + insidePropertyValue = false; output.push(ch); - newline_between_properties ? print.newLine() : print.singleSpace(); + if (!eatWhitespace(true)) { + print.newLine(); + } } else if (ch === '(') { // may be a url if (lookBack("url")) { output.push(ch); @@ -377,9 +466,7 @@ } } else { parenLevel++; - if (isAfterSpace) { - print.singleSpace(); - } + print.preserveSingleSpace(); output.push(ch); eatWhitespace(); } @@ -388,38 +475,58 @@ parenLevel--; } else if (ch === ',') { output.push(ch); - eatWhitespace(); - if (!insideRule && selectorSeparatorNewline && parenLevel < 1) { + if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) { print.newLine(); } else { print.singleSpace(); } + } else if ((ch === '>' || ch === '+' || ch === '~') && + !insidePropertyValue && parenLevel < 1) { + //handle combinator spacing + if (space_around_combinator) { + print.singleSpace(); + output.push(ch); + print.singleSpace(); + } else { + output.push(ch); + eatWhitespace(); + // squash extra whitespace + if (ch && whiteRe.test(ch)) { + ch = ''; + } + } } else if (ch === ']') { output.push(ch); } else if (ch === '[') { - if (isAfterSpace) { - print.singleSpace(); - } + print.preserveSingleSpace(); output.push(ch); } else if (ch === '=') { // no whitespace before or after - eatWhitespace() - ch = '='; - output.push(ch); - } else { - if (isAfterSpace) { - print.singleSpace(); + eatWhitespace(); + output.push('='); + if (whiteRe.test(ch)) { + ch = ''; } - + } else { + print.preserveSingleSpace(); output.push(ch); } } - var sweetCode = output.join('').replace(/[\r\n\t ]+$/, ''); + var sweetCode = ''; + if (basebaseIndentString) { + sweetCode += basebaseIndentString; + } + + sweetCode += output.join('').replace(/[\r\n\t ]+$/, ''); // establish end_with_newline if (end_with_newline) { - sweetCode += "\n"; + sweetCode += '\n'; + } + + if (eol !== '\n') { + sweetCode = sweetCode.replace(/[\n]/g, eol); } return sweetCode; @@ -461,4 +568,4 @@ global.css_beautify = css_beautify; } -}()); +}()); \ No newline at end of file diff --git a/codemirror-overwrites/addon/search/match-highlighter.js b/codemirror-overwrites/addon/search/match-highlighter.js new file mode 100644 index 00000000..f1b49d92 --- /dev/null +++ b/codemirror-overwrites/addon/search/match-highlighter.js @@ -0,0 +1,198 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// Highlighting text that matches the selection +// +// Defines an option highlightSelectionMatches, which, when enabled, +// will style strings that match the selection throughout the +// document. +// +// The option can be set to true to simply enable it, or to a +// {minChars, style, wordsOnly, showToken, delay} object to explicitly +// configure it. minChars is the minimum amount of characters that should be +// selected for the behavior to occur, and style is the token style to +// apply to the matches. This will be prefixed by "cm-" to create an +// actual CSS class name. If wordsOnly is enabled, the matches will be +// highlighted only if the selected text is a word. showToken, when enabled, +// will cause the current token to be highlighted when nothing is selected. +// delay is used to specify how much time to wait, in milliseconds, before +// 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")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./matchesonscrollbar"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var defaults = { + style: "matchhighlight", + minChars: 2, + delay: 100, + wordsOnly: false, + annotateScrollbar: false, + showToken: false, + trim: true + } + + function State(options) { + this.options = {} + for (var name in defaults) + this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] + this.overlay = this.timeout = null; + this.matchesonscroll = null; + this.active = false; + } + + CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + removeOverlay(cm); + clearTimeout(cm.state.matchHighlighter.timeout); + cm.state.matchHighlighter = null; + cm.off("cursorActivity", cursorActivity); + cm.off("focus", onFocus) + } + if (val) { + var state = cm.state.matchHighlighter = new State(val); + if (cm.hasFocus()) { + state.active = true + highlightMatches(cm) + } else { + cm.on("focus", onFocus) + } + cm.on("cursorActivity", cursorActivity); + } + }); + + function cursorActivity(cm) { + var state = cm.state.matchHighlighter; + if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) + } + + function onFocus(cm) { + var state = cm.state.matchHighlighter + if (!state.active) { + state.active = true + scheduleHighlight(cm, state) + } + } + + function scheduleHighlight(cm, state) { + clearTimeout(state.timeout); + state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); + } + + 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) */ + if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { + var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; + state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, + {className: "CodeMirror-selection-highlight-scrollbar"}); + } + } + + function removeOverlay(cm) { + var state = cm.state.matchHighlighter; + if (state.overlay) { + cm.removeOverlay(state.overlay); + state.overlay = null; + if (state.matchesonscroll) { + state.matchesonscroll.clear(); + state.matchesonscroll = null; + } + } + } + + function highlightMatches(cm) { + cm.operation(function() { + var state = cm.state.matchHighlighter; + 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); + } + } + return; + } + removeOverlay(cm); + /* STYLUS: hack end */ + var from = cm.getCursor("from"), to = cm.getCursor("to"); + if (from.line != to.line) return; + if (state.options.wordsOnly && !isWord(cm, from, to)) return; + var selection = cm.getRange(from, to) + if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") + if (selection.length >= state.options.minChars) + addOverlay(cm, selection, false, state.options.style); + }); + } + + function isWord(cm, from, to) { + var str = cm.getRange(from, to); + if (str.match(/^\w+$/) !== null) { + if (from.ch > 0) { + var pos = {line: from.line, ch: from.ch - 1}; + var chr = cm.getRange(pos, from); + if (chr.match(/\W/) === null) return false; + } + if (to.ch < cm.getLine(from.line).length) { + var pos = {line: to.line, ch: to.ch + 1}; + var chr = cm.getRange(to, pos); + if (chr.match(/\W/) === null) return false; + } + return true; + } else return false; + } + + function boundariesAround(stream, re) { + return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && + (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; + 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); + } + return style; + } + /* STYLUS: hack end (part 3) */ + stream.next(); + stream.skipTo(query.charAt(0)) || stream.skipToEnd(); + }}; + } +}); diff --git a/codemirror/LICENSE b/codemirror/LICENSE index 1bca6bfe..ff7db4b9 100644 --- a/codemirror/LICENSE +++ b/codemirror/LICENSE @@ -1,3 +1,5 @@ +MIT License + Copyright (C) 2017 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/codemirror/addon/comment/comment.js b/codemirror/addon/comment/comment.js index d71cf436..568e639d 100644 --- a/codemirror/addon/comment/comment.js +++ b/codemirror/addon/comment/comment.js @@ -46,12 +46,17 @@ // Rough heuristic to try and detect lines that are part of multi-line string function probablyInsideString(cm, pos, line) { - return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line) + return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line) + } + + function getMode(cm, pos) { + var mode = cm.getMode() + return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos) } CodeMirror.defineExtension("lineComment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var firstLine = self.getLine(from.line); if (firstLine == null || probablyInsideString(self, from, firstLine)) return; @@ -95,7 +100,7 @@ CodeMirror.defineExtension("blockComment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var startString = options.blockCommentStart || mode.blockCommentStart; var endString = options.blockCommentEnd || mode.blockCommentEnd; if (!startString || !endString) { @@ -129,7 +134,7 @@ CodeMirror.defineExtension("uncomment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end); // Try finding line comments @@ -171,9 +176,11 @@ 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(Pos(start, open + 1))) || - !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1)))) + !/comment/.test(self.getTokenTypeAt(insideStart)) || + !/comment/.test(self.getTokenTypeAt(insideEnd)) || + self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1) return false; // Avoid killing block comments completely outside the selection. diff --git a/codemirror/addon/lint/lint.js b/codemirror/addon/lint/lint.js index c1f1702f..e5ee7477 100644 --- a/codemirror/addon/lint/lint.js +++ b/codemirror/addon/lint/lint.js @@ -140,7 +140,11 @@ if (options.async || getAnnotations.async) { lintAsync(cm, getAnnotations, passOptions) } else { - updateLinting(cm, getAnnotations(cm.getValue(), passOptions, cm)); + var annotations = getAnnotations(cm.getValue(), passOptions, cm); + if (annotations.then) annotations.then(function(issues) { + updateLinting(cm, issues); + }); + else updateLinting(cm, annotations); } } diff --git a/codemirror/addon/scroll/annotatescrollbar.js b/codemirror/addon/scroll/annotatescrollbar.js index 5e748e81..f2276fc7 100644 --- a/codemirror/addon/scroll/annotatescrollbar.js +++ b/codemirror/addon/scroll/annotatescrollbar.js @@ -77,17 +77,21 @@ curLine = pos.line; curLineObj = cm.getLineHandle(curLine); } - if (wrapping && curLineObj.height > singleLineH) + if ((curLineObj.widgets && curLineObj.widgets.length) || + (wrapping && curLineObj.height > singleLineH)) return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; var topY = cm.heightAtLine(curLineObj, "local"); return topY + (top ? 0 : curLineObj.height); } + var lastLine = cm.lastLine() if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { var ann = anns[i]; + if (ann.to.line > lastLine) continue; var top = nextTop || getY(ann.from, true) * hScale; var bottom = getY(ann.to, false) * hScale; while (i < anns.length - 1) { + if (anns[i + 1].to.line > lastLine) break; nextTop = getY(anns[i + 1].from, true) * hScale; if (nextTop > bottom + .9) break; ann = anns[++i]; diff --git a/codemirror/keymap/emacs.js b/codemirror/keymap/emacs.js index 57cf6e85..2d5fe1b8 100644 --- a/codemirror/keymap/emacs.js +++ b/codemirror/keymap/emacs.js @@ -371,7 +371,9 @@ "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd", "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace", "Alt-/": "autocomplete", - "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto", + "Enter": "newlineAndIndent", + "Ctrl-J": repeated(function(cm) { cm.replaceSelection("\n", "end"); }), + "Tab": "indentAuto", "Alt-G G": function(cm) { var prefix = getPrefix(cm, true); diff --git a/codemirror/keymap/sublime.js b/codemirror/keymap/sublime.js index 3d112ab9..0ce89558 100644 --- a/codemirror/keymap/sublime.js +++ b/codemirror/keymap/sublime.js @@ -152,18 +152,25 @@ var text = cm.getRange(from, to); var query = fullWord ? new RegExp("\\b" + text + "\\b") : text; var cur = cm.getSearchCursor(query, to); - if (cur.findNext()) { - cm.addSelection(cur.from(), cur.to()); - } else { + var found = cur.findNext(); + if (!found) { cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0)); - if (cur.findNext()) - cm.addSelection(cur.from(), cur.to()); + found = cur.findNext(); } + if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to())) + return CodeMirror.Pass + cm.addSelection(cur.from(), cur.to()); } if (fullWord) cm.state.sublimeFindFullWord = cm.doc.sel; }; + function isSelectedRange(ranges, from, to) { + for (var i = 0; i < ranges.length; i++) + if (ranges[i].from() == from && ranges[i].to() == to) return true + return false + } + var mirror = "(){}[]"; function selectBetweenBrackets(cm) { var ranges = cm.listSelections(), newRanges = [] diff --git a/codemirror/keymap/vim.js b/codemirror/keymap/vim.js index 34570bb8..b2c404d4 100644 --- a/codemirror/keymap/vim.js +++ b/codemirror/keymap/vim.js @@ -142,7 +142,7 @@ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, - { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, + { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'}, { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'}, { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, @@ -1245,11 +1245,13 @@ } } function onPromptKeyUp(e, query, close) { - var keyName = CodeMirror.keyName(e), up; + var keyName = CodeMirror.keyName(e), up, offset; if (keyName == 'Up' || keyName == 'Down') { up = keyName == 'Up' ? true : false; + offset = e.target ? e.target.selectionEnd : 0; query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; close(query); + if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); } else { if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') vimGlobalState.searchHistoryController.reset(); @@ -1281,6 +1283,8 @@ clearInputState(cm); close(); cm.focus(); + } else if (keyName == 'Up' || keyName == 'Down') { + CodeMirror.e_stop(e); } else if (keyName == 'Ctrl-U') { // Ctrl-U clears input. CodeMirror.e_stop(e); @@ -1344,7 +1348,7 @@ exCommandDispatcher.processCommand(cm, input); } function onPromptKeyDown(e, input, close) { - var keyName = CodeMirror.keyName(e), up; + var keyName = CodeMirror.keyName(e), up, offset; if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || (keyName == 'Backspace' && input == '')) { vimGlobalState.exCommandHistoryController.pushInput(input); @@ -1355,9 +1359,12 @@ cm.focus(); } if (keyName == 'Up' || keyName == 'Down') { + CodeMirror.e_stop(e); up = keyName == 'Up' ? true : false; + offset = e.target ? e.target.selectionEnd : 0; input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; close(input); + if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); } else if (keyName == 'Ctrl-U') { // Ctrl-U clears input. CodeMirror.e_stop(e); @@ -1620,9 +1627,8 @@ return findNext(cm, prev/** prev */, query, motionArgs.repeat); }, goToMark: function(cm, _head, motionArgs, vim) { - var mark = vim.marks[motionArgs.selectedCharacter]; - if (mark) { - var pos = mark.find(); + var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter); + if (pos) { return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; } return null; @@ -3966,6 +3972,17 @@ return {top: from.line, bottom: to.line}; } + function getMarkPos(cm, vim, markName) { + if (markName == '\'') { + var history = cm.doc.history.done; + var event = history[history.length - 2]; + return event && event.ranges && event.ranges[0].head; + } + + var mark = vim.marks[markName]; + return mark && mark.find(); + } + var ExCommandDispatcher = function() { this.buildCommandMap_(); }; @@ -4074,11 +4091,10 @@ case '$': return cm.lastLine(); case '\'': - var mark = cm.state.vim.marks[inputStream.next()]; - if (mark && mark.find()) { - return mark.find().line; - } - throw new Error('Mark not set'); + var markName = inputStream.next(); + var markPos = getMarkPos(cm, cm.state.vim, markName); + if (!markPos) throw new Error('Mark not set'); + return markPos.line; default: inputStream.backUp(1); return undefined; @@ -4147,8 +4163,8 @@ var mapping = { keys: lhs, type: 'keyToEx', - exArgs: { input: rhs.substring(1) }, - user: true}; + exArgs: { input: rhs.substring(1) } + }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); } else { @@ -4156,8 +4172,7 @@ var mapping = { keys: lhs, type: 'keyToKey', - toKeys: rhs, - user: true + toKeys: rhs }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); @@ -4178,8 +4193,7 @@ var keys = lhs; for (var i = 0; i < defaultKeymap.length; i++) { if (keys == defaultKeymap[i].keys - && defaultKeymap[i].context === ctx - && defaultKeymap[i].user) { + && defaultKeymap[i].context === ctx) { defaultKeymap.splice(i, 1); return; } @@ -4310,25 +4324,27 @@ showConfirm(cm, regInfo); }, sort: function(cm, params) { - var reverse, ignoreCase, unique, number; + var reverse, ignoreCase, unique, number, pattern; function parseArgs() { if (params.argString) { var args = new CodeMirror.StringStream(params.argString); if (args.eat('!')) { reverse = true; } if (args.eol()) { return; } if (!args.eatSpace()) { return 'Invalid arguments'; } - var opts = args.match(/[a-z]+/); - if (opts) { - opts = opts[0]; - ignoreCase = opts.indexOf('i') != -1; - unique = opts.indexOf('u') != -1; - var decimal = opts.indexOf('d') != -1 && 1; - var hex = opts.indexOf('x') != -1 && 1; - var octal = opts.indexOf('o') != -1 && 1; + var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/); + if (!opts && !args.eol()) { return 'Invalid arguments'; } + if (opts[1]) { + ignoreCase = opts[1].indexOf('i') != -1; + unique = opts[1].indexOf('u') != -1; + var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1; + var hex = opts[1].indexOf('x') != -1 && 1; + var octal = opts[1].indexOf('o') != -1 && 1; if (decimal + hex + octal > 1) { return 'Invalid arguments'; } number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; } - if (args.match(/\/.*\//)) { return 'patterns not supported'; } + if (opts[2]) { + pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : ''); + } } } var err = parseArgs(); @@ -4342,14 +4358,18 @@ var curStart = Pos(lineStart, 0); var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); var text = cm.getRange(curStart, curEnd).split('\n'); - var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : + var numberRegex = pattern ? pattern : + (number == 'decimal') ? /(-?)([\d]+)/ : (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : (number == 'octal') ? /([0-7]+)/ : null; var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; var numPart = [], textPart = []; - if (number) { + if (number || pattern) { for (var i = 0; i < text.length; i++) { - if (numberRegex.exec(text[i])) { + var matchPart = pattern ? text[i].match(pattern) : null; + if (matchPart && matchPart[0] != '') { + numPart.push(matchPart); + } else if (!pattern && numberRegex.exec(text[i])) { numPart.push(text[i]); } else { textPart.push(text[i]); @@ -4368,8 +4388,17 @@ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); return anum - bnum; } - numPart.sort(compareFn); - textPart.sort(compareFn); + function comparePatternFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); } + return (a[0] < b[0]) ? -1 : 1; + } + numPart.sort(pattern ? comparePatternFn : compareFn); + if (pattern) { + for (var i = 0; i < numPart.length; i++) { + numPart[i] = numPart[i].input; + } + } else if (!number) { textPart.sort(compareFn); } text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); if (unique) { // Remove duplicate lines var textOld = text; diff --git a/codemirror/lib/codemirror.css b/codemirror/lib/codemirror.css index 2a6a2622..b962b383 100644 --- a/codemirror/lib/codemirror.css +++ b/codemirror/lib/codemirror.css @@ -223,11 +223,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} cursor: default; z-index: 4; } -.CodeMirror-gutter-wrapper { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } .CodeMirror-lines { cursor: text; @@ -272,6 +269,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-widget {} +.CodeMirror-rtl pre { direction: rtl; } + .CodeMirror-code { outline: none; } diff --git a/codemirror/mode/css/css.js b/codemirror/mode/css/css.js index 90de4ee7..02cc93d9 100644 --- a/codemirror/mode/css/css.js +++ b/codemirror/mode/css/css.js @@ -28,6 +28,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { colorKeywords = parserConfig.colorKeywords || {}, valueKeywords = parserConfig.valueKeywords || {}, allowNested = parserConfig.allowNested, + lineComment = parserConfig.lineComment, supportsAtComponent = parserConfig.supportsAtComponent === true; var type, override; @@ -253,6 +254,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) { }; states.pseudo = function(type, stream, state) { + if (type == "meta") return "pseudo"; + if (type == "word") { override = "variable-3"; return state.context.type; @@ -407,6 +410,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { electricChars: "}", blockCommentStart: "/*", blockCommentEnd: "*/", + lineComment: lineComment, fold: "brace" }; }); @@ -663,7 +667,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square", "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", - "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table", + "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group", "table-row", "table-row-group", "tamil", @@ -730,6 +734,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { valueKeywords: valueKeywords, fontProperties: fontProperties, allowNested: true, + lineComment: "//", tokenHooks: { "/": function(stream, state) { if (stream.eat("/")) { @@ -772,6 +777,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { valueKeywords: valueKeywords, fontProperties: fontProperties, allowNested: true, + lineComment: "//", tokenHooks: { "/": function(stream, state) { if (stream.eat("/")) { diff --git a/codemirror/mode/css/index.html b/codemirror/mode/css/index.html index 2d2b9b07..0d85311f 100644 --- a/codemirror/mode/css/index.html +++ b/codemirror/mode/css/index.html @@ -64,7 +64,7 @@ code { diff --git a/csslint/WARNING.txt b/csslint/WARNING.txt new file mode 100644 index 00000000..bf059aff --- /dev/null +++ b/csslint/WARNING.txt @@ -0,0 +1,12 @@ +1. Until https://github.com/CSSLint/parser-lib/issues/229 is fixed, manually replace: + + while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) { + + in "_function: function()" with + + while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN && lt !== Tokens.EOF) { + +2. Apply our hacks unless supported natively: + + * Support :any(), :-webkit-any(), :-moz-any() + * Support @supports inside @-moz-document diff --git a/csslint/csslint-worker.js b/csslint/csslint-worker.js index e55704cc..bb6be9df 100644 --- a/csslint/csslint-worker.js +++ b/csslint/csslint-worker.js @@ -2781,7 +2781,7 @@ Parser.prototype = function() { //functionText += this._term(); lt = tokenStream.peek(); - while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) { + while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN && lt !== Tokens.EOF) { tokenStream.get(); functionText += tokenStream.token().value; lt = tokenStream.peek(); diff --git a/dom.js b/dom.js new file mode 100644 index 00000000..d2475415 --- /dev/null +++ b/dom.js @@ -0,0 +1,110 @@ +'use strict'; + +if (!navigator.userAgent.includes('Windows')) { + document.documentElement.classList.add('non-windows'); +} + +// polyfill for old browsers to enable [...results] and for-of +for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) { + if (!type.prototype[Symbol.iterator]) { + type.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; + } +} + + +function onDOMready() { + if (document.readyState != 'loading') { + return Promise.resolve(); + } + return new Promise(resolve => { + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + resolve(); + }); + }); +} + + +function scrollElementIntoView(element) { + // 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); + } +} + + +function animateElement(element, {className, remove = false}) { + return new Promise(resolve => { + element.addEventListener('animationend', function _() { + element.removeEventListener('animationend', _); + element.classList.remove(className); + // TODO: investigate why animation restarts if the elements is removed in .then() + if (remove) { + element.remove(); + } + resolve(); + }); + element.classList.add(className); + }); +} + + +function enforceInputRange(element) { + const min = Number(element.min); + const max = Number(element.max); + const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true})); + const onChange = ({type}) => { + if (type == 'input' && element.checkValidity()) { + doNotify(); + } else if (type == 'change' && !element.checkValidity()) { + element.value = Math.max(min, Math.min(max, Number(element.value))); + doNotify(); + } + }; + element.addEventListener('change', onChange); + element.addEventListener('input', onChange); +} + + +function $(selector, base = document) { + // we have ids with . like #manage.onlyEnabled which looks like #id.class + // so since getElementById is superfast we'll try it anyway + const byId = selector.startsWith('#') && document.getElementById(selector.slice(1)); + return byId || base.querySelector(selector); +} + + +function $$(selector, base = document) { + return [...base.querySelectorAll(selector)]; +} + + +function $element(opt) { + // tag: string, default 'div', may include namespace like 'ns#tag' + // appendChild: element or an array of elements + // dataset: object + // any DOM property: assigned as is + const [ns, tag] = opt.tag && opt.tag.includes('#') + ? opt.tag.split('#') + : [null, opt.tag]; + const element = ns + ? document.createElementNS(ns == 'SVG' || ns == 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag) + : document.createElement(tag || 'div'); + (opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild]) + .forEach(child => child && element.appendChild(child)); + delete opt.appendChild; + delete opt.tag; + if (opt.dataset) { + Object.assign(element.dataset, opt.dataset); + delete opt.dataset; + } + if (ns) { + for (const attr in opt) { + element.setAttributeNS(null, attr, opt[attr]); + } + } else { + Object.assign(element, opt); + } + return element; +} diff --git a/edit.html b/edit.html index e12f34ae..f64f31d2 100644 --- a/edit.html +++ b/edit.html @@ -1,6 +1,14 @@ - + + + + + + + + + @@ -9,6 +17,7 @@ + @@ -40,27 +49,29 @@ body { margin: 0; - font: 9pt arial,sans-serif; + font: 12px arial,sans-serif; } /************ header ************/ #header { - height: calc(100vh - 30px); + width: 280px; + height: 100vh; overflow: auto; - width: 15rem; position: fixed; top: 0; - padding: 0.95rem; + padding: 15px; border-right: 1px dashed #AAA; -webkit-box-shadow: 0 0 3rem -1.2rem black; + box-sizing: border-box; } #header h1 { margin-top: 0; } #sections { - padding-left: 18rem; + padding-left: 280px; } #sections h2 { - margin-top: 0.5rem; + margin-top: 1rem; + margin-left: 1.7rem; } .aligned { display: table-row; @@ -90,23 +101,39 @@ #url:not([href^="http"]) { display: none; } + #save-button { + opacity: .5; + pointer-events: none; + } + .dirty #save-button { + opacity: 1; + pointer-events: all; + } .svg-icon { cursor: pointer; vertical-align: middle; transition: fill .5s; + width: 16px; + height: 16px; } - .svg-icon:not(.applies-to-help):not(.dismiss) { + .svg-icon:not(.dismiss) { margin-left: 0.2rem; } h2 .svg-icon, label .svg-icon { margin-top: -2px; } - .svg-icon.info:hover { - fill: #000000; + .svg-icon.info { + width: 14px; + height: 16px; + } + .svg-icon:hover, + .svg-icon.info { + fill: #666; + } + .svg-icon, + .svg-icon.info:hover { + fill: #000; } - a:hover .svg-icon.installed, .svg-icon.dismiss:hover { - fill: hsl(0, 0%, 40%); - } #enabled { margin-left: 0; vertical-align: middle; @@ -183,6 +210,15 @@ .CodeMirror-search-hint { color: #888; } + body[data-highlight-selection-matches="token"] .cm-matchhighlight-approved .cm-matchhighlight, + body[data-highlight-selection-matches="token"] .CodeMirror-selection-highlight-scrollbar { + animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98); + animation-fill-mode: both; + } + body[data-highlight-selection-matches="selection"] .cm-matchhighlight-approved .cm-matchhighlight, + body[data-highlight-selection-matches="selection"] .CodeMirror-selection-highlight-scrollbar { + background-color: rgba(1, 151, 193, 0.1); + } @-webkit-keyframes highlight { from { background-color: #ff9; @@ -191,6 +227,18 @@ background-color: none; } } + @keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes fadein-match-highlighter { + from { background-color: transparent; } + to { background-color: rgba(1, 151, 193, 0.1); } + } .resize-grip { position: absolute; display: block; @@ -245,6 +293,62 @@ .applies-to img { vertical-align: bottom; } + .test-regexp { + display: none; + } + .has-regexp .test-regexp { + display: inline-block; + } + .regexp-report summary, .regexp-report div { + cursor: pointer; + outline: none; + } + .regexp-report mark { + background-color: rgba(255, 255, 0, .5); + } + .regexp-report details { + margin-left: 1rem; + } + .regexp-report details:not(:last-child) { + margin-bottom: 1rem; + } + .regexp-report summary { + font-weight: bold; + margin-left: -1rem; + margin-bottom: .5rem; + outline: none; + cursor: default; + } + .regexp-report details[data-type="full"] { + color: darkgreen; + } + .regexp-report details[data-type="partial"] { + color: darkgray; + } + .regexp-report details[data-type="invalid"] { + color: maroon; + } + .regexp-report details details { + margin-left: 2rem; + margin-top: .5rem; + } + .regexp-report .svg-icon { + position: absolute; + margin-top: -1px; + } + .regexp-report details div:hover { + text-decoration: underline; + text-decoration-skip: ink; + } + .regexp-report details div img { + width: 16px; + max-height: 16px; + position: absolute; + margin-left: -20px; + margin-top: -1px; + animation: fadein 1s cubic-bezier(.03, .67, .08, .94); + animation-fill-mode: both; + } /************ help popup ************/ #help-popup { top: 3rem; @@ -269,16 +373,16 @@ font-weight: bold; background-color: rgba(0,0,0,0.05); margin: -0.5rem -0.5rem 0.5rem; - padding: 0.5rem; + padding: .5rem 32px .5rem .5rem; } #help-popup .contents { max-height: calc(100vh - 8rem); overflow-y: auto; } - #help-popup .close-icon { + #help-popup .dismiss { position: absolute; right: 4px; - top: 4px; + top: .5em; } .keymap-list { @@ -514,13 +618,14 @@
    +