Merge pull request #50 from schomery/cm-and-cache

CodeMirror 5.24 + Improve style caching performance + many other features + code refactoring
This commit is contained in:
tophf 2017-05-02 17:21:43 +03:00 committed by GitHub
commit 43eb4a1314
82 changed files with 8470 additions and 4000 deletions

View File

@ -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

108
.eslintrc
View File

@ -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]

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Získat styly na userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Získat pomoc</a>",
"message": "<a href='https://userstyles.org'>Získat styly na userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Získat pomoc</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Styles von userstyles.org beziehen</a><br><a href='https://userstyles.org/help/stylish_chrome'>Hilfeseite anzeigen</a>",
"message": "<a href='https://userstyles.org'>Styles von userstyles.org beziehen</a> | <a href='http://add0n.com/stylus.html'>Hilfeseite anzeigen</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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": {

View File

@ -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 <a href='https:\/\/github.com\/CSSLint\/csslint' target='_blank'>CSSLint<\/a> with these rules enabled:",
"message": "The issues found by <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> 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": "<a href='https:\/\/userstyles.org'>Get styles on userstyles.org<\/a> | <a href='https:\/\/userstyles.org\/help\/stylish_chrome'>Get help<\/a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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 <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)."
},
"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"
}
}

View File

@ -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": "<a href='https://userstyles.org'>Obtener estilos en userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Obtener ayuda</a>",
"message": "<a href='https://userstyles.org'>Obtener estilos en userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Obtener ayuda</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Verkrijg stijlen op userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Verkrijg hulp</a>",
"message": "<a href='https://userstyles.org'>Verkrijg stijlen op userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Verkrijg hulp</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Скачать стили с userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Справка</a>",
"message": "<a href='https://userstyles.org'>Скачать стили с userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Справка</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Преузмите стилове са userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Помоћ</a>",
"message": "<a href='https://userstyles.org'>Преузмите стилове са userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Помоћ</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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"

View File

@ -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": "<a href='https://userstyles.org'>访问 userstyles.org 获取样式</a> | <a href='https://userstyles.org/help/stylish_chrome'>获取帮助</a>",
"message": "<a href='https://userstyles.org'>访问 userstyles.org 获取样式</a> | <a href='http://add0n.com/stylus.html'>获取帮助</a>",
"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": {

View File

@ -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": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"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": {

494
apply.js
View File

@ -1,317 +1,305 @@
// 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);
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"
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 {
removeStyle(request.style.id, document);
chrome.runtime.sendMessage(request, callback);
}
}
function applyOnMessage(request, sender, sendResponse) {
if (request.styles == 'DIY') {
// Do-It-Yourself tells our built-in pages to fetch the styles directly
// which is faster because IPC messaging JSON-ifies everything internally
requestStyles({}, styles => {
request.styles = styles;
applyOnMessage(request);
});
return;
}
switch (request.method) {
case 'styleDeleted':
removeStyle(request);
break;
case 'styleUpdated':
if (request.codeIsUpdated === false) {
applyStyleState(request.style);
break;
}
case "styleAdded":
if (request.style.enabled) {
chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles);
removeStyle({id: request.style.id, retire: true});
requestStyles({id: request.style.id});
} else {
removeStyle(request.style);
}
break;
case "styleApply":
case 'styleAdded':
if (request.style.enabled) {
requestStyles({id: request.style.id});
}
break;
case 'styleApply':
applyStyles(request.styles);
break;
case "styleReplaceAll":
replaceAll(request.styles, document);
case 'styleReplaceAll':
replaceAll(request.styles);
break;
case "styleDisableAll":
disableAll(request.disableAll);
case 'prefChanged':
if ('disableAll' in request.prefs) {
doDisableAll(request.prefs.disableAll);
}
if ('exposeIframes' in request.prefs) {
doExposeIframes(request.prefs.exposeIframes);
}
break;
case "ping":
case 'ping':
sendResponse(true);
break;
}
}
function disableAll(disable) {
if (!disable === !g_disableAll) {
function doDisableAll(disable = disableAll) {
if (!disable === !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")) {
disableAll = disable;
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
if (stylesheet.ownerNode.matches(`STYLE.stylus[id^="${ID_PREFIX}"]`)
&& stylesheet.disabled != disable) {
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 doExposeIframes(state = exposeIframes) {
if (state === exposeIframes || window == parent) {
return;
}
exposeIframes = state;
const attr = document.documentElement.getAttribute('stylus-iframe');
if (state && attr != '') {
document.documentElement.setAttribute('stylus-iframe', '');
} else if (!state && attr == '') {
document.documentElement.removeAttribute('stylus-iframe');
}
}
function removeStyle(id, doc) {
var e = doc.getElementById("stylus-" + id);
delete g_styleElements["stylus-" + id];
if (e) {
e.remove();
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();
}
if (doc == document && Object.keys(g_styleElements).length == 0) {
iframeObserver.disconnect();
}
getDynamicIFrames(doc).forEach(function(iframe) {
removeStyle(id, iframe.contentDocument);
});
}
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
function retireStyle(id, doc) {
var deadID = "ghost-" + id;
if (!doc) {
doc = document;
retiredStyleIds.push(deadID);
delete g_styleElements["stylus-" + id];
const deadID = 'ghost-' + id;
el.id = ID_PREFIX + deadID;
// in case something went wrong and new style was never applied
setTimeout(removeStyle.bind(null, deadID, doc), 1000);
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
} else {
el.remove();
}
var e = doc.getElementById("stylus-" + id);
if (e) {
e.id = "stylus-" + deadID;
}
getDynamicIFrames(doc).forEach(function(iframe) {
retireStyle(id, iframe.contentDocument);
});
styleElements.delete(ID_PREFIX + id);
disabledElements.delete(id);
retiredStyleTimers.delete(id);
}
function applyStyles(styleHash) {
if (!styleHash) { // Chrome is starting up
function applyStyles(styles) {
if (!styles) {
// Chrome is starting up
requestStyles();
return;
}
if ("disableAll" in styleHash) {
disableAll(styleHash.disableAll);
delete styleHash.disableAll;
if ('disableAll' in styles) {
doDisableAll(styles.disableAll);
delete styles.disableAll;
}
for (var styleId in styleHash) {
applySections(styleId, styleHash[styleId]);
if ('exposeIframes' in styles) {
doExposeIframes(styles.exposeIframes);
delete styles.exposeIframes;
}
if (Object.keys(g_styleElements).length) {
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 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));
// which is already autogenerated at this moment
ROOT = document.head;
}
for (const id in styles) {
applySections(id, styles[id]);
}
document.addEventListener("DOMContentLoaded", onDOMContentLoaded);
initDocRewriteObserver();
if (retiredStyleTimers.size) {
setTimeout(() => {
for (const [id, timer] of retiredStyleTimers.entries()) {
removeStyle({id});
clearTimeout(timer);
}
if (retiredStyleIds.length) {
setTimeout(function() {
while (retiredStyleIds.length) {
removeStyle(retiredStyleIds.shift(), document);
}
}, 0);
});
}
}
function onDOMContentLoaded() {
addDocumentStylesToAllIFrames();
iframeObserver.start();
}
function applySections(styleId, sections) {
var styleElement = document.getElementById("stylus-" + styleId);
// Already there.
if (styleElement) {
let el = document.getElementById(ID_PREFIX + styleId);
if (el) {
return;
}
if (document.documentElement instanceof SVGSVGElement) {
// SVG document, make an SVG style element.
styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style");
// 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 {
// 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;
}
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);
// 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 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 addStyleElement(el) {
if (ROOT && !document.getElementById(el.id)) {
ROOT.appendChild(el);
el.disabled = disableAll;
}
}
function addDocumentStylesToAllIFrames() {
getDynamicIFrames(document).forEach(addDocumentStylesToIFrame);
}
// 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 = {};
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);
replaceAll(newStyles, doc, true);
}
if (pass2) {
oldStyles.forEach(function(style) { style.remove(); });
}
oldStyles.forEach(el => el.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();
function initDocRewriteObserver() {
if (isOwnPage || docRewriteObserver || !styleElements.size) {
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);
// 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;
}
}
}
});
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);
}
}
}
docRewriteObserver.observe(document, {childList: true});
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
setTimeout(() => {
if (document.documentElement != ROOT) {
reinjectStyles();
}
});
}
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();
const port = chrome.runtime.connect();
if (port) {
port.disconnect();
return;
@ -319,34 +307,30 @@ function initObserver() {
// we're orphaned due to an extension update
// we can detach the mutation observer
iframeObserver.takeRecords();
iframeObserver.disconnect();
iframeObserver = null;
if (docRewriteObserver) {
docRewriteObserver.disconnect();
}
// we can detach event listeners
document.removeEventListener("DOMContentLoaded", onDOMContentLoaded);
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
// 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',
// we can destroy our globals in this context to free up memory
[ // functions
'addStyleElement',
'addStyleToIFrameSrcDoc',
'applyOnMessage',
'applySections',
'applyStyles',
'disableAll',
'getDynamicIFrames',
'iframeIsDynamic',
'iframeIsLoadingSrcDoc',
'initObserver',
'applyStyleState',
'doDisableAll',
'initDocRewriteObserver',
'orphanCheck',
'removeStyle',
'replaceAll',
'requestStyles',
'retireStyle'
].forEach(fn => window[fn] = null);
// we can destroy global variables
g_styleElements = iframeObserver = retiredStyleIds = null;
}
// variables
'ROOT',
'disabledElements',
'retiredStyleTimers',
'styleElements',
'docRewriteObserver',
].forEach(fn => (window[fn] = null));
}

View File

@ -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);
// 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.webNavigation.onBeforeNavigate.addListener(data =>
webNavigationListener(null, data));
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;
}
});
chrome.webNavigation.onCommitted.addListener(data =>
webNavigationListener('styleApply', data));
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
webNavigationListener('styleReplaceAll', data));
// 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;
}
});
}
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
webNavigationListener('styleReplaceAll', data));
// 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});
});
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);
}
// Get the DB so that any first run actions will be performed immediately when the background page loads.
getDatabase(function() {}, reportError);
// 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) {
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);
prefs.set('openEditInWindow', win.tabs.length == 1);
});
}
});
});
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) {});
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]());
}
// *************************************************************************
{
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'});
}
// *************************************************************************
// 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'});
},
}
});
{
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 {
delete options.method;
getActiveTab(function(tab) {
// re-use an active new tab page
chrome.tabs[tab.url == "chrome://newtab/" ? "update" : "create"](options);
});
chrome.contextMenus.remove(id, ignoreChromeError);
}
});
} else {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
}
}, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean'));
}
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);
})
// *************************************************************************
// [re]inject content scripts
{
const NTP = 'chrome://newtab/';
const PING = {method: 'ping'};
const ALL_URLS = '<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)
));
}
}
});
injectContentScripts();
function injectContentScripts() {
const contentScripts = chrome.app.getDetails().content_scripts;
for (let cs of contentScripts) {
cs.matches = cs.matches.map(m => m == '<all_urls>' ? 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 == '<all_urls>' || tab.url.match(m)) {
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => {
if (!pong) {
chrome.tabs.executeScript(tab.id, {
const injectCS = (cs, tabId) => {
chrome.tabs.executeScript(tabId, {
file: cs.js[0],
runAt: cs.run_at,
allFrames: cs.all_frames,
}, result => chrome.runtime.lastError); // ignore lastError just in case
matchAboutBlank: cs.match_about_blank,
}, ignoreChromeError);
};
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;
}
});
// inject the content script just once
break;
}
};
chrome.tabs.query({}, tabs =>
tabs.forEach(tab =>
contentScripts.forEach(cs =>
pingCS(cs, tab))));
}
// *************************************************************************
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;
}
}

View File

@ -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 (!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]));
if (numStyles) {
invalidateCache(true);
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);
}
return new Promise(resolve => {
proceed();
function proceed() {
const nextStyle = json.shift();
if (nextStyle) {
saveStyle(nextStyle, {notify: false}).then(style => {
handleUpdate(style);
setTimeout(proceed, 0);
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 {
refreshAllTabs().then(() => {
setTimeout(alert, 100, numStyles + ' styles installed/updated');
resolve(numStyles);
});
}
undoNextId();
}
});
}
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;
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;
}
}
}
let url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
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();
}
});
}
});
}));
}
}
$('#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]});
}
},
});

View File

@ -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, <einar@jsbeautifier.org>
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;
}
}());

View File

@ -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
@ -41,11 +41,13 @@
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, {
@ -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();
output.push(ch);
newline_after_open_brace ? print.newLine() : print.singleSpace();
};
print["}"] = function(ch) {
newline_before_close_brace ? print.newLine() : print.singleSpace();
print.singleSpace();
output.push(ch);
if (!eatWhitespace(true)) {
print.newLine();
}
};
print["}"] = function(newline) {
if (newline) {
print.newLine();
}
output.push('}');
if (!eatWhitespace(true)) {
print.newLine();
}
};
print._lastCharWhitespace = function() {
@ -218,16 +280,18 @@
};
print.newLine = function(keepWhitespace) {
if (!keepWhitespace) {
print.trim();
}
if (output.length) {
output.push('\n');
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);
}
}
};
print.singleSpace = function() {
if (output.length && !print._lastCharWhitespace()) {
@ -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("");
var header = indentLevel === 0;
if (isAfterNewline || header) {
print.newLine();
}
output.push(eatComment());
print.newLine();
if (header) {
@ -278,10 +350,12 @@
output.push(eatComment());
print.newLine();
} else if (ch === '@') {
// pass along the space we found as a separate item
if (isAfterSpace) {
print.singleSpace();
}
print.preserveSingleSpace();
// deal with less propery mixins @{...}
if (peek() === '{') {
output.push(eatString('}'));
} else {
output.push(ch);
// strip trailing space, if present, for hash property checks
@ -295,7 +369,7 @@
print.singleSpace();
}
variableOrRule = variableOrRule.replace(/\s$/, '')
variableOrRule = variableOrRule.replace(/\s$/, '');
// might be a nesting at-rule
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
@ -304,14 +378,18 @@
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(':');
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);
eatWhitespace();
output.push('=');
if (whiteRe.test(ch)) {
ch = '';
}
} else {
if (isAfterSpace) {
print.singleSpace();
}
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;

View File

@ -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();
}};
}
});

View File

@ -1,3 +1,5 @@
MIT License
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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];

View File

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

View File

@ -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 = []

View File

@ -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;

View File

@ -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;
}

View File

@ -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("/")) {

View File

@ -64,7 +64,7 @@ code {
</textarea></form>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
extraKeys: {"Ctrl-Space": "autocomplete"},
extraKeys: {"Ctrl-Space": "autocomplete"}
});
</script>

12
csslint/WARNING.txt Normal file
View File

@ -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

View File

@ -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();

110
dom.js Normal file
View File

@ -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;
}

180
edit.html
View File

@ -1,6 +1,14 @@
<html>
<html id="stylus">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<script src="dom.js"></script>
<script src="messaging.js"></script>
<script src="prefs.js"></script>
<script src="localization.js"></script>
<script src="apply.js"></script>
<script src="edit.js"></script>
<script src="codemirror/lib/codemirror.js"></script>
<link rel="stylesheet" href="codemirror/lib/codemirror.css">
<script src="codemirror/mode/css/css.js"></script>
@ -9,6 +17,7 @@
<link rel="stylesheet" href="codemirror/addon/search/matchesonscrollbar.css">
<script src="codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="codemirror-overwrites/addon/search/match-highlighter.js"></script>
<script src="codemirror/addon/dialog/dialog.js"></script>
<script src="codemirror/addon/search/searchcursor.js"></script>
<script src="codemirror/addon/search/search.js"></script>
@ -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,22 +101,38 @@
#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;
}
a:hover .svg-icon.installed, .svg-icon.dismiss:hover {
fill: hsl(0, 0%, 40%);
.svg-icon:hover,
.svg-icon.info {
fill: #666;
}
.svg-icon,
.svg-icon.info:hover {
fill: #000;
}
#enabled {
margin-left: 0;
@ -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 @@
<br>
<div class="applies-to">
<label i18n-text="appliesLabel">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="applies-to-help svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg>
<svg class="svg-icon info applies-to-help"><use xlink:href="#svg-icon-help"/></svg>
</label>
<ul class="applies-to-list"></ul>
</div>
<button class="remove-section" i18n-text="sectionRemove"></button>
<button class="add-section" i18n-text="sectionAdd"></button>
<button class="beautify-section" i18n-text="styleBeautify"></button>
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
</div>
</template>
<template data-id="find">
@ -552,20 +657,18 @@
<template data-id="jumpToLine">
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
</template>
<script src="storage.js"></script>
<script src="messaging.js"></script>
<script src="localization.js"></script>
<script src="apply.js"></script>
<script src="edit.js"></script>
<template data-id="regexpTestPartial">
<a target="_blank" href="https://github.com/stylish-userstyles/stylish/wiki/Applying-styles-to-specific-sites#advanced-matching-with-regular-expressions"><svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></a>
</template>
</head>
<body id="stylus-edit">
<div id="header">
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info">
<div id="basic-info-name">
<input id="name" class="style-contributor" i18n-placeholder="styleMissingName">
<a id="url" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-icon installed" fill="#000000" height="16" width="16" viewBox="0 0 8 8"><path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path></svg></a>
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div>
<div id="basic-info-enabled">
<input type="checkbox" id="enabled" class="style-contributor">
@ -579,7 +682,7 @@
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div>
<div>
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="to-mozilla-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></h2>
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
<button id="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button>
</div>
@ -605,21 +708,42 @@
<div class="option aligned">
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
<select data-option="keyMap" id="editor.keyMap"></select>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="keyMap-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg>
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</div>
<div class="option aligned">
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
<select data-option="theme" id="editor.theme"></select>
</div>
<div class="option aligned">
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
<select data-option="highlightSelectionMatches" id="editor.matchHighlight">
<option i18n-text="cm_matchHighlightToken" value="token">
<option i18n-text="cm_matchHighlightSelection" value="selection">
<option i18n-text="genericDisabledLabel" value="">
</select>
</div>
</section>
<section id="lint"><h2 i18n-text="issues">: <span id="issue-count"></span><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="lint-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></h2><div></div></section>
<section id="lint"><h2 i18n-text="issues">: <span id="issue-count"></span><svg id="lint-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2><div></div></section>
</div>
<section id="sections">
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="sections-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></h2>
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
</section>
<div id="help-popup">
<div class="title"></div><svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="close-icon svg-icon dismiss" fill="#000000" height="16" width="12" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path></svg>
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg></svg>
<div class="contents"></div>
</div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
</symbol>
<symbol id="svg-icon-help" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt">
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
</symbol>
<symbol id="svg-icon-close" height="16" width="12" viewBox="0 0 12 16">
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
</symbol>
</svg>
</body>
</html>

432
edit.js
View File

@ -1,4 +1,5 @@
/* globals stringAsRegExp */
/* eslint no-tabs: 0, no-var: 0, indent: [2, tab, {VariableDeclarator: 0, SwitchCase: 1}], quotes: 0 */
/* global CodeMirror */
"use strict";
var styleId = null;
@ -11,6 +12,9 @@ var useHistoryBack; // use browser history back when "back to manage" is click
var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"};
var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"};
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
onBackgroundReady();
// make querySelectorAll enumeration code readable
["forEach", "some", "indexOf", "map"].forEach(function(method) {
NodeList.prototype[method]= Array.prototype[method];
@ -29,10 +33,22 @@ Array.prototype.rotate = function(amount) { // negative amount == rotate left
var r = this.slice(-amount, this.length);
Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
return r;
}
};
Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }});
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
new MutationObserver((mutations, observer) => {
const themeElement = document.getElementById("cm-theme");
if (themeElement) {
themeElement.href = prefs.get("editor.theme") == "default" ? ""
: "codemirror/theme/" + prefs.get("editor.theme") + ".css";
observer.disconnect();
}
}).observe(document, {subtree: true, childList: true});
getCodeMirrorThemes();
// reroute handling to nearest editor when keypress resolves to one of these commands
var hotkeyRerouter = {
commands: {
@ -130,6 +146,11 @@ function initCodeMirror() {
var CM = CodeMirror;
var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0;
// CodeMirror miserably fails on keyMap="" so let's ensure it's not
if (!prefs.get('editor.keyMap')) {
prefs.reset('editor.keyMap');
}
// default option values
Object.assign(CM.defaults, {
mode: 'css',
@ -138,6 +159,7 @@ function initCodeMirror() {
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
matchBrackets: true,
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")},
lintReportDelay: prefs.get("editor.lintReportDelay"),
styleActiveLine: true,
@ -229,38 +251,30 @@ function initCodeMirror() {
return this.display.wrapper.parentNode;
};
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
var theme = prefs.get("editor.theme");
document.getElementById("cm-theme").href = theme == "default" ? "" : "codemirror/theme/" + theme + ".css";
// initialize global editor controls
document.addEventListener("DOMContentLoaded", function() {
function optionsHtmlFromArray(options) {
return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join("");
}
var themeControl = document.getElementById("editor.theme");
var bg = chrome.extension.getBackgroundPage();
if (bg && bg.codeMirrorThemes) {
themeControl.innerHTML = optionsHtmlFromArray(bg.codeMirrorThemes);
const themeList = localStorage.codeMirrorThemes;
if (themeList) {
themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/));
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
const theme = prefs.get("editor.theme");
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
getCodeMirrorThemes(function(themes) {
getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
themeControl.innerHTML = optionsHtmlFromArray(themes);
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
});
}
document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort());
document.getElementById("options").addEventListener("change", acmeEventListener, false);
setupLivePrefs(
document.querySelectorAll("#options *[data-option][id^='editor.']")
.map(function(option) { return option.id })
);
});
setupLivePrefs();
hotkeyRerouter.setState(true);
}
initCodeMirror();
function acmeEventListener(event) {
var el = event.target;
@ -287,7 +301,7 @@ function acmeEventListener(event) {
el.selectedIndex = 0;
break;
}
var url = chrome.extension.getURL("codemirror/theme/" + value + ".css");
var url = chrome.runtime.getURL("codemirror/theme/" + value + ".css");
if (themeLink.href == url) { // preloaded in initCodeMirror()
break;
}
@ -302,13 +316,23 @@ function acmeEventListener(event) {
}, 100);
})();
return;
case "highlightSelectionMatches":
switch (value) {
case 'token':
case 'selection':
document.body.dataset[option] = value;
value = {showToken: value == 'token' && /[#.\-\w]/, annotateScrollbar: true};
break;
default:
value = null;
}
}
CodeMirror.setOption(option, value);
}
// replace given textarea with the CodeMirror editor
function setupCodeMirror(textarea, index) {
var cm = CodeMirror.fromTextArea(textarea);
var cm = CodeMirror.fromTextArea(textarea, {lint: null});
cm.on("change", indicateCodeChange);
cm.on("blur", function(cm) {
@ -324,6 +348,7 @@ function setupCodeMirror(textarea, index) {
hotkeyRerouter.setState(false);
cm.display.wrapper.classList.add("CodeMirror-active");
});
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
var resizeGrip = cm.display.wrapper.appendChild(document.createElement("div"));
resizeGrip.className = "resize-grip";
@ -395,6 +420,10 @@ document.addEventListener("wheel", function(event) {
chrome.tabs.query({currentWindow: true}, function(tabs) {
var windowId = tabs[0].windowId;
if (prefs.get("openEditInWindow")) {
if (sessionStorage.saveSizeOnClose && 'left' in prefs.get('windowPosition', {})) {
// window was reopened via Ctrl-Shift-T etc.
chrome.windows.update(windowId, prefs.get('windowPosition'));
}
if (tabs.length == 1 && window.history.length == 1) {
chrome.windows.getAll(function(windows) {
if (windows.length > 1) {
@ -414,7 +443,7 @@ chrome.tabs.query({currentWindow: true}, function(tabs) {
});
});
getActiveTab(function(tab) {
getActiveTab().then(tab => {
useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href;
});
@ -497,6 +526,23 @@ function addSection(event, section) {
appliesTo.addEventListener("change", onChange);
appliesTo.addEventListener("input", onChange);
toggleTestRegExpVisibility();
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
div.querySelector('.test-regexp').onclick = showRegExpTester;
function toggleTestRegExpVisibility() {
const show = [...appliesTo.children].some(item =>
!item.matches('.applies-to-everything') &&
item.querySelector('.applies-type').value == 'regexp' &&
item.querySelector('.applies-value').value.trim());
div.classList.toggle('has-regexp', show);
appliesTo.oninput = appliesTo.oninput || show && (event => {
if (event.target.matches('.applies-value')
&& event.target.parentElement.querySelector('.applies-type').value == 'regexp') {
showRegExpTester(null, div);
}
});
}
var sections = document.getElementById("sections");
if (event) {
var clickedSection = getSectionForChild(event.target);
@ -857,36 +903,32 @@ function getEditorInSight(nearbyElement) {
function updateLintReport(cm, delay) {
if (delay == 0) {
// immediately show pending csslint messages in onbeforeunload and save
update.call(cm);
update(cm);
return;
}
if (delay > 0) {
// give csslint some time to find the issues, e.g. 500 (1/10 of our default 5s)
// by settings its internal delay to 1ms and restoring it back later
var lintOpt = editors[0].state.lint.options;
setTimeout((function(opt, delay) {
opt.delay = delay == 1 ? opt.delay : delay; // options object is shared between editors
update(this);
}).bind(cm, lintOpt, lintOpt.delay), delay);
lintOpt.delay = 1;
setTimeout(cm => { cm.performLint(); update(cm) }, delay, cm);
return;
}
var state = cm.state.lint;
if (!state) {
return;
}
// user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms)
// or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed
var state = cm.state.lint;
clearTimeout(state.reportTimeout);
state.reportTimeout = setTimeout(update.bind(cm), state.options.delay + 100);
state.reportTimeout = setTimeout(update, state.options.delay + 100, cm);
state.postponeNewIssues = delay == undefined || delay == null;
function update() { // this == cm
var scope = this ? [this] : editors;
function update(cm) {
var scope = cm ? [cm] : editors;
var changed = false;
var fixedOldIssues = false;
scope.forEach(function(cm) {
var state = cm.state.lint;
var state = cm.state.lint || {};
var oldMarkers = state.markedLast || {};
var newMarkers = {};
var html = state.marked.length == 0 ? "" : "<tbody>" +
var html = !state.marked || state.marked.length == 0 ? "" : "<tbody>" +
state.marked.map(function(mark) {
var info = mark.__annotation;
var isActiveLine = info.from.line == cm.getCursor().line;
@ -938,7 +980,7 @@ function renderLintReport(someBlockChanged) {
var newContent = content.cloneNode(false);
var issueCount = 0;
editors.forEach(function(cm, index) {
if (cm.state.lint.html) {
if (cm.state.lint && cm.state.lint.html) {
var newBlock = newContent.appendChild(document.createElement("table"));
var html = "<caption>" + label + " " + (index+1) + "</caption>" + cm.state.lint.html;
newBlock.innerHTML = html;
@ -993,7 +1035,7 @@ function beautify(event) {
doBeautify();
} else {
var script = document.head.appendChild(document.createElement("script"));
script.src = "beautify/beautify-css.js";
script.src = "beautify/beautify-css-mod.js";
script.onload = doBeautify;
}
function doBeautify() {
@ -1023,6 +1065,7 @@ function beautify(event) {
if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
delete cm.beautifyChange[cm.changeGeneration()];
cm.undo();
cm.scrollIntoView(cm.getCursor());
undoable |= cm.beautifyChange[cm.changeGeneration()];
}
});
@ -1031,6 +1074,9 @@ function beautify(event) {
scope.forEach(function(cm) {
setTimeout(function() {
const pos = options.translate_positions =
[].concat.apply([], cm.doc.sel.ranges.map(r =>
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
var text = cm.getValue();
var newText = exports.css_beautify(text, options);
if (newText != text) {
@ -1039,6 +1085,11 @@ function beautify(event) {
cm.beautifyChange = {};
}
cm.setValue(newText);
const selections = [];
for (let i = 0; i < pos.length; i += 2) {
selections.push({anchor: pos[i], head: pos[i + 1]});
}
cm.setSelections(selections);
cm.beautifyChange[cm.changeGeneration()] = true;
undoButton.disabled = false;
}
@ -1065,48 +1116,67 @@ function beautify(event) {
}
}
window.addEventListener("load", init, false);
document.addEventListener("DOMContentLoaded", init);
function init() {
initCodeMirror();
var params = getParams();
if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
// This is an add
tE("heading", "addStyleTitle");
var section = {code: ""}
for (var i in CssToProperty) {
if (params[i]) {
section[CssToProperty[i]] = [params[i]];
}
}
window.onload = () => {
window.onload = null;
addSection(null, section);
// default to enabled
document.getElementById("enabled").checked = true
tE("heading", "addStyleTitle");
initHooks();
};
return;
}
// This is an edit
tE("heading", "editStyleHeading", null, false);
requestStyle();
function requestStyle() {
chrome.runtime.sendMessage({method: "getStyles", id: params.id}, function callback(styles) {
if (!styles) { // Chrome is starting up and shows edit.html
requestStyle();
return;
getStylesSafe({id: params.id}).then(styles => {
let style = styles[0];
if (!style) {
style = {id: null, sections: []};
history.replaceState({}, document.title, location.pathname);
}
var style = styles[0];
styleId = style.id;
initWithStyle(style);
setStyleMeta(style);
window.onload = () => {
window.onload = null;
initWithStyle({style});
};
if (document.readyState != 'loading') {
window.onload();
}
});
}
}
function initWithStyle(style) {
function setStyleMeta(style) {
document.getElementById("name").value = style.name;
document.getElementById("enabled").checked = style.enabled;
document.getElementById("url").href = style.url;
}
function initWithStyle({style, codeIsUpdated}) {
setStyleMeta(style);
if (codeIsUpdated === false) {
setCleanGlobal();
updateTitle();
return;
}
// if this was done in response to an update, we need to clear existing sections
getSections().forEach(function(div) { div.remove(); });
var queue = style.sections.length ? style.sections : [{code: ""}];
var queue = style.sections.length ? style.sections.slice() : [{code: ""}];
var queueStart = new Date().getTime();
// after 100ms the sections will be added asynchronously
while (new Date().getTime() - queueStart <= 100 && queue.length) {
@ -1123,7 +1193,11 @@ function initWithStyle(style) {
function add() {
var sectionDiv = addSection(null, queue.shift());
maximizeCodeHeight(sectionDiv, !queue.length);
updateLintReport(sectionDiv.CodeMirror, prefs.get("editor.lintDelay"));
const cm = sectionDiv.CodeMirror;
setTimeout(() => {
cm.setOption('lint', CodeMirror.defaults.lint);
updateLintReport(cm, 0);
}, prefs.get("editor.lintDelay"));
}
}
@ -1149,11 +1223,28 @@ function initHooks() {
document.querySelector("#lint h2").addEventListener("click", toggleLintReport);
}
document.querySelectorAll(
'input:not([type]), input[type="text"], input[type="search"], input[type="number"]')
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
setupGlobalSearch();
setCleanGlobal();
updateTitle();
}
function toggleContextMenuDelete(event) {
if (event.button == 2 && prefs.get('editor.contextDelete')) {
chrome.contextMenus.update('editor.contextDelete', {
enabled: Boolean(
this.selectionStart != this.selectionEnd ||
this.somethingSelected && this.somethingSelected()
),
}, ignoreChromeError);
}
}
function maximizeCodeHeight(sectionDiv, isLast) {
var cm = sectionDiv.CodeMirror;
var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
@ -1254,14 +1345,14 @@ function save() {
}
var name = document.getElementById("name").value;
var enabled = document.getElementById("enabled").checked;
var request = {
method: "saveStyle",
saveStyleSafe({
id: styleId,
name: name,
enabled: enabled,
reason: 'editSave',
sections: getSectionsHashes()
};
chrome.runtime.sendMessage(request, saveComplete);
})
.then(saveComplete);
}
function getSectionsHashes() {
@ -1348,7 +1439,7 @@ function fromMozillaFormat() {
function doImport() {
var replaceOldStyle = this.name == "import-replace";
popup.querySelector(".close-icon").click();
popup.querySelector(".dismiss").onclick();
var mozStyle = trimNewLines(popup.codebox.getValue());
var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n");
var sectionStack = [{code: "", start: {line: 1, col: 1}}];
@ -1423,15 +1514,20 @@ function fromMozillaFormat() {
}
}
function doAddSection(section) {
section.code = section.code.trim();
// don't add empty sections
if (!section.code
&& !section.urls
&& !section.urlPrefixes
&& !section.domains
&& !section.regexps) {
return;
}
if (!firstAddedCM) {
if (!initFirstSection(section)) {
return;
}
}
// don't add empty sections
if (!(section.code || section.urls || section.urlPrefixes || section.domains || section.regexps)) {
return;
}
setCleanItem(addSection(null, section), false);
firstAddedCM = firstAddedCM || editors.last;
}
@ -1532,8 +1628,8 @@ function showKeyMapHelp() {
cell.innerHTML = cell.textContent;
});
}
function mergeKeyMaps(merged) {
[].slice.call(arguments, 1).forEach(function(keyMap) {
function mergeKeyMaps(merged, ...more) {
more.forEach(keyMap => {
if (typeof keyMap == "string") {
keyMap = CodeMirror.keyMap[keyMap];
}
@ -1567,6 +1663,111 @@ function showLintHelp() {
);
}
function showRegExpTester(event, section = getSectionForChild(this)) {
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const cachedRegexps = showRegExpTester.cachedRegexps =
showRegExpTester.cachedRegexps || new Map();
const regexps = [...section.querySelector('.applies-to-list').children]
.map(item =>
!item.matches('.applies-to-everything') &&
item.querySelector('.applies-type').value == 'regexp' &&
item.querySelector('.applies-value').value.trim())
.filter(item => item)
.map(text => {
const rxData = Object.assign({text}, cachedRegexps.get(text));
if (!rxData.urls) {
cachedRegexps.set(text, Object.assign(rxData, {
rx: tryRegExp(text),
urls: new Map(),
}));
}
return rxData;
});
chrome.tabs.onUpdated.addListener(function _(tabId, info) {
if (document.querySelector('.regexp-report')) {
if (info.url) {
showRegExpTester(event, section);
}
} else {
chrome.tabs.onUpdated.removeListener(_);
}
});
chrome.tabs.query({}, tabs => {
const supported = tabs.map(tab => tab.url)
.filter(url => URLS.supported.test(url));
const unique = [...new Set(supported).values()];
for (const rxData of regexps) {
const {rx, urls} = rxData;
if (rx) {
const urlsNow = new Map();
for (const url of unique) {
const match = urls.get(url) || (url.match(rx) || [])[0];
if (match) {
urlsNow.set(url, match);
}
}
rxData.urls = urlsNow;
}
}
const moreInfoLink = template.regexpTestPartial.outerHTML;
const stats = {
full: {data: [], label: t('styleRegexpTestFull')},
partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink},
none: {data: [], label: t('styleRegexpTestNone')},
invalid: {data: [], label: t('styleRegexpTestInvalid')},
};
for (const {text, rx, urls} of regexps) {
if (!rx) {
stats.invalid.data.push({text});
continue;
}
if (!urls.size) {
stats.none.data.push({text});
continue;
}
const full = [];
const partial = [];
for (const [url, match] of urls.entries()) {
const faviconUrl = url.startsWith(URLS.ownOrigin)
? OWN_ICON
: GET_FAVICON_URL + new URL(url).hostname;
const icon = `<img src="${faviconUrl}">`;
if (match.length == url.length) {
full.push(`<div>${icon + url}</div>`);
} else {
partial.push(`<div>${icon}<mark>${match}</mark>` +
url.substr(match.length) + '</div>');
}
}
if (full.length) {
stats.full.data.push({text, urls: full});
}
if (partial.length) {
stats.partial.data.push({text, urls: partial});
}
}
showHelp(t('styleRegexpTestTitle'),
'<div class="regexp-report">' +
Object.keys(stats).map(type => (!stats[type].data.length ? '' :
`<details open data-type="${type}">
<summary>${stats[type].label}</summary>` +
stats[type].data.map(({text, urls}) => (!urls ? text :
`<details open><summary>${text}</summary>${urls.join('')}</details>`
)).join('<br>') +
'</details>'
)).join('') +
'</div>');
document.querySelector('.regexp-report').onclick = event => {
const target = event.target.closest('a, .regexp-report div');
if (target) {
openURL({url: target.href || target.textContent});
event.preventDefault();
}
};
});
}
function showHelp(title, text) {
var div = document.getElementById("help-popup");
div.classList.remove("big");
@ -1575,14 +1776,16 @@ function showHelp(title, text) {
if (getComputedStyle(div).display == "none") {
document.addEventListener("keydown", closeHelp);
div.querySelector(".close-icon").onclick = closeHelp; // avoid chaining on multiple showHelp() calls
div.querySelector(".dismiss").onclick = closeHelp; // avoid chaining on multiple showHelp() calls
}
div.style.display = "block";
return div;
function closeHelp(e) {
if (e.type == "click" || (e.keyCode == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
if (!e
|| e.type == "click"
|| ((e.keyCode || e.which) == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
div.style.display = "";
document.querySelector(".contents").innerHTML = "";
document.removeEventListener("keydown", closeHelp);
@ -1625,11 +1828,21 @@ function getParams() {
return params;
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
chrome.runtime.onMessage.addListener(onRuntimeMessage);
function onRuntimeMessage(request) {
switch (request.method) {
case "styleUpdated":
if (styleId && styleId == request.id) {
initWithStyle(request.style);
if (styleId && styleId == request.style.id && request.reason != 'editSave') {
if ((request.style.sections[0] || {}).code === null) {
// the code-less style came from notifyAllTabs
onBackgroundReady().then(() => {
request.style = BG.cachedStyles.byId.get(request.style.id);
initWithStyle(request);
});
} else {
initWithStyle(request);
}
}
break;
case "styleDeleted":
@ -1640,14 +1853,93 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
}
break;
case "prefChanged":
if (request.prefName == "editor.smartIndent") {
CodeMirror.setOption("smartIndent", request.value);
if ('editor.smartIndent' in request.prefs) {
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
}
break;
case 'editDeleteText':
document.execCommand('delete');
break;
}
}
});
function getComputedHeight(el) {
var compStyle = getComputedStyle(el);
return el.getBoundingClientRect().height +
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
}
function getCodeMirrorThemes() {
if (!chrome.runtime.getPackageDirectoryEntry) {
const themes = Promise.resolve([
chrome.i18n.getMessage('defaultTheme'),
'3024-day',
'3024-night',
'abcdef',
'ambiance',
'ambiance-mobile',
'base16-dark',
'base16-light',
'bespin',
'blackboard',
'cobalt',
'colorforth',
'dracula',
'duotone-dark',
'duotone-light',
'eclipse',
'elegant',
'erlang-dark',
'hopscotch',
'icecoder',
'isotope',
'lesser-dark',
'liquibyte',
'material',
'mbo',
'mdn-like',
'midnight',
'monokai',
'neat',
'neo',
'night',
'panda-syntax',
'paraiso-dark',
'paraiso-light',
'pastel-on-dark',
'railscasts',
'rubyblue',
'seti',
'solarized',
'the-matrix',
'tomorrow-night-bright',
'tomorrow-night-eighties',
'ttcn',
'twilight',
'vibrant-ink',
'xq-dark',
'xq-light',
'yeti',
'zenburn',
]);
localStorage.codeMirrorThemes = themes.join(' ');
}
return new Promise(resolve => {
chrome.runtime.getPackageDirectoryEntry(rootDir => {
rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => {
themeDir.createReader().readEntries(entries => {
const themes = [
chrome.i18n.getMessage('defaultTheme')
].concat(
entries.filter(entry => entry.isFile)
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map(entry => entry.name.replace(/\.css$/, ''))
);
localStorage.codeMirrorThemes = themes.join(' ');
resolve(themes);
});
});
});
});
}

View File

@ -1,11 +0,0 @@
healthCheck();
function healthCheck() {
chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) {
if (ok === undefined) { // Chrome is starting up
healthCheck();
} else if (!ok && confirm(t("dbError"))) {
window.open("http://userstyles.org/dberror");
}
});
}

View File

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 532 B

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

View File

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

View File

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 449 B

View File

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 787 B

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

Before

Width:  |  Height:  |  Size: 837 B

After

Width:  |  Height:  |  Size: 837 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 1010 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/world_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,180 +1,212 @@
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) {
if (response.length == 0) {
sendEvent("styleCanBeInstalledChrome");
} else {
var installedStyle = response[0];
// maybe an update is needed
// use the md5 if available
var md5Url = getMeta("stylish-md5-url");
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
getResource(md5Url, function(md5) {
if (md5 == installedStyle.originalMd5) {
sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl});
} else {
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
'use strict';
document.addEventListener('stylishUpdateChrome', onUpdateClicked);
document.addEventListener('stylishInstallChrome', onInstallClicked);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// orphaned content script check
if (msg.method == 'ping') {
sendResponse(true);
}
});
} else {
getResource(getMeta("stylish-code-chrome"), function(code) {
// this would indicate a failure (a style with settings?).
if (code == null) {
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
new MutationObserver((mutations, observer) => {
if (document.body) {
observer.disconnect();
chrome.runtime.sendMessage({
method: 'getStyles',
url: getMeta('stylish-id-url') || location.href
}, checkUpdatability);
}
var json = JSON.parse(code);
if (json.sections.length == installedStyle.sections.length) {
if (json.sections.every(function(section) {
return installedStyle.sections.some(function(installedSection) {
return sectionsAreEqual(section, installedSection);
});
})) {
// everything's the same
sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl});
}).observe(document.documentElement, {childList: true});
function checkUpdatability([installedStyle]) {
if (!installedStyle) {
sendEvent('styleCanBeInstalledChrome');
return;
};
}
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
const md5Url = getMeta('stylish-md5-url');
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
getResource(md5Url).then(md5 => {
reportUpdatable(md5 != installedStyle.originalMd5);
});
}
}
});
function sectionsAreEqual(a, b) {
if (a.code != b.code) {
return false;
}
return ["urls", "urlPrefixes", "domains", "regexps"].every(function(attribute) {
return arraysAreEqual(a[attribute], b[attribute]);
} else {
getResource(getMeta('stylish-code-chrome')).then(code => {
reportUpdatable(code === null ||
!styleSectionsEqual(JSON.parse(code), installedStyle));
});
}
function arraysAreEqual(a, b) {
// treat empty array and undefined as equivalent
if (typeof a == "undefined")
return (typeof b == "undefined") || (b.length == 0);
if (typeof b == "undefined")
return (typeof a == "undefined") || (a.length == 0);
function reportUpdatable(isUpdatable) {
sendEvent(
isUpdatable
? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome',
{
updateUrl: installedStyle.updateUrl
}
);
}
}
function sendEvent(type, detail = null) {
detail = {detail};
if (typeof cloneInto != 'undefined') {
// Firefox requires explicit cloning, however USO can't process our messages anyway
// because USO tries to use a global "event" variable deprecated in Firefox
detail = cloneInto(detail, document); // eslint-disable-line no-undef
}
onDOMready().then(() => {
document.dispatchEvent(new CustomEvent(type, detail));
});
}
function onInstallClicked() {
if (!orphanCheck || !orphanCheck()) {
return;
}
getResource(getMeta('stylish-description'))
.then(name => saveStyleCode('styleInstall', name))
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
}
function onUpdateClicked() {
if (!orphanCheck || !orphanCheck()) {
return;
}
chrome.runtime.sendMessage({
method: 'getStyles',
url: getMeta('stylish-id-url') || location.href,
}, ([style]) => {
saveStyleCode('styleUpdate', style.name, {id: style.id});
});
}
function saveStyleCode(message, name, addProps) {
return new Promise(resolve => {
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
return;
}
getResource(getMeta('stylish-code-chrome')).then(code => {
chrome.runtime.sendMessage(
Object.assign(JSON.parse(code), addProps, {
method: 'saveStyle',
reason: 'update',
}),
() => sendEvent('styleInstalledChrome')
);
resolve();
});
});
}
function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null;
}
function getResource(url) {
return new Promise(resolve => {
if (url.startsWith('#')) {
resolve(document.getElementById(url.slice(1)).textContent);
} else {
chrome.runtime.sendMessage({method: 'download', url}, resolve);
}
});
}
function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
}
if (a.length != b.length) {
return false;
}
return a.every(function(entry) {
return b.indexOf(entry) != -1;
});
const checkedInB = [];
return a.every(sectionA => b.some(sectionB => {
if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
checkedInB.push(sectionB);
return true;
}
}));
function propertiesEqual(secA, secB) {
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
return false;
}
}
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
}
function sendEvent(type, data) {
if (typeof data == "undefined") {
data = null;
}
var stylishEvent = new CustomEvent(type, {detail: data});
document.dispatchEvent(stylishEvent);
function equalOrEmpty(a, b, telltale, comparator) {
const typeA = a && typeof a[telltale] == 'function';
const typeB = b && typeof b[telltale] == 'function';
return (
(a === null || a === undefined || (typeA && !a.length)) &&
(b === null || b === undefined || (typeB && !b.length))
) || typeA && typeB && a.length == b.length && comparator(a, b);
}
document.addEventListener("stylishUpdateChrome", stylishUpdateChrome);
function stylishInstallChrome() {
orphanCheck();
getResource(getMeta("stylish-description"), function(name) {
if (confirm(chrome.i18n.getMessage('styleInstall', [name]))) {
getResource(getMeta("stylish-code-chrome"), function(code) {
// check for old style json
var json = JSON.parse(code);
json.method = "saveStyle";
chrome.runtime.sendMessage(json, function(response) {
sendEvent("styleInstalledChrome");
});
});
getResource(getMeta("stylish-install-ping-url-chrome"));
}
});
}
document.addEventListener("stylishInstallChrome", stylishInstallChrome);
function stylishUpdateChrome() {
orphanCheck();
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) {
var style = response[0];
if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) {
getResource(getMeta("stylish-code-chrome"), function(code) {
var json = JSON.parse(code);
json.method = "saveStyle";
json.id = style.id;
chrome.runtime.sendMessage(json, function() {
sendEvent("styleInstalledChrome");
});
});
}
});
}
function getMeta(name) {
var e = document.querySelector("link[rel='" + name + "']");
return e ? e.getAttribute("href") : null;
}
function getResource(url, callback) {
if (url.indexOf("#") == 0) {
if (callback) {
callback(document.getElementById(url.substring(1)).innerText);
}
return;
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && callback) {
if (xhr.status >= 400) {
callback(null);
} else {
callback(xhr.responseText);
function arrayMirrors(array1, array2) {
for (const el of array1) {
if (array2.indexOf(el) < 0) {
return false;
}
}
for (const el of array2) {
if (array1.indexOf(el) < 0) {
return false;
}
if (url.length > 2000) {
var parts = url.split("?");
xhr.open("POST", parts[0], true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send(parts[1]);
} else {
xhr.open("GET", url, true);
xhr.send();
}
return true;
}
}
/* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */
(function (es) {
es.forEach(e => {
[...e.childNodes].filter(n => n.nodeType == 3).forEach(n => {
n.nodeValue = n.nodeValue.replace('Stylish', 'Stylus');
function onDOMready() {
if (document.readyState != 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
})([
...document.querySelectorAll('div[id^="stylish-installed-style-not-installed-"]'),
...document.querySelectorAll('div[id^="stylish-installed-style-needs-update-"]')
]);
}
// orphaned content script check
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) =>
msg.method == 'ping' && sendResponse(true));
function orphanCheck() {
var port = chrome.runtime.connect();
const port = chrome.runtime.connect();
if (port) {
port.disconnect();
return;
return true;
}
// we're orphaned due to an extension update
// we can detach event listeners
document.removeEventListener('stylishUpdateChrome', stylishUpdateChrome);
document.removeEventListener('stylishInstallChrome', stylishInstallChrome);
document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
document.removeEventListener('stylishInstallChrome', onInstallClicked);
// 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
[
'arraysAreEqual',
'checkUpdatability',
'getMeta',
'getResource',
'onDOMready',
'onInstallClicked',
'onUpdateClicked',
'orphanCheck',
'sectionsAreEqual',
'saveStyleCode',
'sendEvent',
'stylishUpdateChrome',
'stylishInstallChrome'
].forEach(fn => window[fn] = null);
'styleSectionsEqual',
].forEach(fn => (window[fn] = null));
}

View File

@ -1,82 +1,121 @@
var template = {};
'use strict';
const template = {};
tDocLoader();
function t(key, params) {
var s = chrome.i18n.getMessage(key, params)
if (s == "") {
throw "Missing string '" + key + "'.";
const cache = !params && t.cache[key];
const s = cache || chrome.i18n.getMessage(key, params);
if (s == '') {
throw `Missing string "${key}"`;
}
if (!params && !cache) {
t.cache[key] = s;
}
return s;
}
function o(key) {
document.write(t(key));
}
function tE(id, key, attr, esc) {
if (attr) {
document.getElementById(id).setAttribute(attr, t(key));
} else if (typeof esc == "undefined" || esc) {
} else if (typeof esc == 'undefined' || esc) {
document.getElementById(id).appendChild(document.createTextNode(t(key)));
} else {
document.getElementById(id).innerHTML = t(key);
}
}
function tHTML(html) {
var node = document.createElement("div");
const node = document.createElement('div');
node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use &nbsp; for an explicit space
tNodeList(node.querySelectorAll("*"));
var child = node.removeChild(node.firstElementChild);
node.remove();
return child;
if (html.includes('i18n-')) {
tNodeList(node.getElementsByTagName('*'));
}
return node.firstElementChild;
}
function tNodeList(nodes) {
for (var n = 0; n < nodes.length; n++) {
var node = nodes[n];
if (node.nodeType != 1) { // not an ELEMENT_NODE
const PREFIX = 'i18n-';
for (let n = nodes.length; --n >= 0;) {
const node = nodes[n];
// skip non-ELEMENT_NODE
if (node.nodeType != 1) {
continue;
}
if (node.localName == "template") {
tNodeList(node.content.querySelectorAll("*"));
template[node.dataset.id] = node.content.firstElementChild;
if (node.localName == 'template') {
const elements = node.content.querySelectorAll('*');
tNodeList(elements);
template[node.dataset.id] = elements[0];
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
const toRemove = [];
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!textNode.nodeValue.trim()) {
toRemove.push(textNode);
}
}
toRemove.forEach(el => el.remove());
continue;
}
for (var a = node.attributes.length - 1; a >= 0; a--) {
var attr = node.attributes[a];
var name = attr.nodeName;
if (name.indexOf("i18n-") != 0) {
for (let a = node.attributes.length; --a >= 0;) {
const attr = node.attributes[a];
const name = attr.nodeName;
if (!name.startsWith(PREFIX)) {
continue;
}
name = name.substr(5); // "i18n-".length
var value = t(attr.value);
switch (name) {
case "text":
const type = name.substr(PREFIX.length);
const value = t(attr.value);
switch (type) {
case 'text':
node.insertBefore(document.createTextNode(value), node.firstChild);
break;
case "html":
node.insertAdjacentHTML("afterbegin", value);
case 'text-append':
node.appendChild(document.createTextNode(value));
break;
case 'html':
node.insertAdjacentHTML('afterbegin', value);
break;
default:
node.setAttribute(name, value);
node.setAttribute(type, value);
}
node.removeAttribute(attr.nodeName);
node.removeAttribute(name);
}
}
}
function tDocLoader() {
t.cache = tryJSONparse(localStorage.L10N) || {};
const cacheLength = Object.keys(t.cache).length;
// localize HEAD
tNodeList(document.querySelectorAll("*"));
tNodeList(document.getElementsByTagName('*'));
// localize BODY
var observer = new MutationObserver(function(mutations) {
for (var m = 0; m < mutations.length; m++) {
tNodeList(mutations[m].addedNodes);
const process = mutations => {
for (const mutation of mutations) {
tNodeList(mutation.addedNodes);
}
});
};
const observer = new MutationObserver(process);
const onLoad = () => {
tDocLoader.stop();
process(observer.takeRecords());
if (cacheLength != Object.keys(t.cache).length) {
localStorage.L10N = JSON.stringify(t.cache);
}
};
tDocLoader.start = () => {
observer.observe(document, {subtree: true, childList: true});
document.addEventListener("DOMContentLoaded", function() {
};
tDocLoader.stop = () => {
observer.disconnect();
tNodeList(document.querySelectorAll("*"));
});
document.removeEventListener('DOMContentLoaded', onLoad);
};
tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
}

779
manage.css Normal file
View File

@ -0,0 +1,779 @@
body {
margin: 0;
font: 12px arial, sans-serif;
/* Firefox: fill the entire page for drag'n'drop to work */
display: flex;
height: 100%;
}
a {
color: #000;
transition: color .5s;
text-decoration-skip: ink;
}
a:hover {
color: #666;
}
#header {
width: 280px;
height: 100vh;
position: fixed;
top: 0;
padding: 15px;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
overflow: auto;
box-sizing: border-box;
z-index: 9;
}
#header h1 {
margin-top: 0;
}
#header a[href^="edit"] {
text-decoration: none;
}
.firefox .chromium-only {
display: none;
}
#installed {
position: relative;
padding-left: 280px;
box-sizing: border-box;
width: 100%;
}
.entry {
margin: 0;
padding: 1.25em 2em;
border-top: 1px solid #ddd;
}
.entry:first-child {
border-top: none;
}
.svg-icon {
cursor: pointer;
vertical-align: middle;
transition: fill .5s;
width: 20px;
height: 20px;
}
.svg-icon:hover {
fill: #000;
}
.svg-icon {
fill: #666;
}
.svg-icon.info {
width: 14px;
height: 16px;
margin-left: .5ex;
}
.homepage {
margin-left: 0.1em;
margin-right: 0.1em;
}
.homepage[href=""] {
display: none;
}
.homepage .svg-icon {
margin-top: -4px;
margin-left: .5ex;
}
.style-name {
margin-top: .25em;
word-break: break-word;
}
.style-name a, .style-edit-link {
text-decoration: none;
}
.style-name-link:hover {
text-decoration: underline;
color: #000;
}
.applies-to {
word-break: break-word;
}
.applies-to,
.actions {
padding-left: 15px;
margin-bottom: 0;
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.actions > * {
margin-bottom: .25rem;
}
.actions > *:not(:last-child) {
margin-right: .25rem;
}
.applies-to label {
margin-right: .5ex;
}
.applies-to .target:hover {
background-color: rgba(128, 128, 128, .15);
}
.applies-to-extra:not([open]) {
display: inline;
margin-left: 1ex;
}
summary {
font-weight: bold;
cursor: pointer;
outline: none;
}
.applies-to-extra summary {
list-style-type: none; /* for FF, allegedly */
}
.applies-to-extra summary::-webkit-details-marker {
display: none;
}
.disabled h2::after {
content: "__MSG_genericDisabledLabel__";
font-weight: normal;
font-size: 11px;
text-transform: lowercase;
background: rgba(128, 128, 128, .2);
padding: 2px 5px 3px;
border-radius: 4px;
margin-left: 1ex;
}
.disabled {
opacity: 0.5;
}
.disabled .disable {
display: none;
}
.enabled .enable {
display: none;
}
/* compact layout */
.newUI #installed {
display: table;
margin-top: .75rem;
margin-bottom: .75rem;
}
.newUI .disabled {
opacity: 1;
}
.newUI .disabled .style-name,
.newUI .disabled .applies-to {
opacity: .5;
}
.newUI .entry {
display: table-row;
}
.newUI .entry:nth-child(2n) {
background-color: rgba(128, 128, 128, 0.05);
}
.newUI .entry > * {
padding: .9rem 0 1rem;
margin: 0;
display: table-cell;
vertical-align: middle;
}
.newUI .checker {
position: relative;
top: 1px;
margin-right: 1ex;
}
.newUI .style-name {
font-size: 14px;
font-family: sans-serif;
text-indent: -2em;
padding-left: 3em;
padding-right: 30px;
}
.newUI .homepage .svg-icon {
position: absolute;
margin-top: 0;
margin-left: -28px;
}
.newUI .actions {
width: 60px;
height: 20px;
white-space: nowrap;
}
.newUI .actions > * {
margin: 0;
}
.newUI .actions .svg-icon {
margin-right: 8px;
}
.newUI .updater-icons > * {
transition: opacity 1s;
display: none;
}
.newUI .entry .svg-icon {
fill: #999;
}
.newUI .entry:hover .svg-icon {
fill: #666;
}
.newUI .entry:hover .svg-icon:hover {
fill: #000;
}
.newUI .checking-update .check-update {
opacity: 0;
display: inline;
pointer-events: none;
}
.newUI .can-update .update,
.newUI .no-update:not(.update-problem):not(.update-done) .up-to-date,
.newUI .no-update.update-problem .check-update,
.newUI .update-done .updated {
display: inline;
}
.newUI .update-done .updated svg {
top: -4px;
position: relative;
/* unprefixed since Chrome 53 */
-webkit-filter: drop-shadow(0 4px 0 currentColor);
filter: drop-shadow(0 5px 0 currentColor);
}
.newUI .can-update .update,
.newUI .no-update.update-problem .check-update {
cursor: pointer;
}
.newUI .can-update[data-details$="locally edited"] .update svg,
.newUI .update-problem .check-update svg {
fill: #ef6969;
}
.newUI .can-update[data-details$="locally edited"]:hover .update svg,
.newUI .entry.update-problem:hover .check-update svg {
fill: #fd4040;
}
.newUI .can-update[data-details$="locally edited"]:hover .update svg:hover,
.newUI .entry.update-problem:hover .check-update svg:hover {
fill: red;
}
.newUI .updater-icons > :not(.check-update):after {
content: attr(title);
position: absolute;
margin-top: 18px;
margin-left: -36px;
padding: 1ex 1.5ex;
border: 1px solid #ded597;
background-color: #fffbd6;
border-radius: 4px;
box-shadow: 2px 3px 10px rgba(0,0,0,.25);
font-size: 90%;
animation: fadeout 10s;
animation-fill-mode: both;
z-index: 999;
}
.newUI .update-problem .check-update:after {
background-color: red;
border: 1px solid #d40000;
color: white;
animation: none;
}
.newUI .can-update .update:after {
animation: none;
}
.newUI .can-update:not([data-details$="locally edited"]) .update:after {
background-color: #c0fff0;
border: 1px solid #89cac9;
}
.newUI .applies-to {
padding-top: .25rem;
padding-bottom: .25rem;
}
.newUI .targets {
overflow: hidden;
}
.newUI .applies-to.expanded .targets {
max-height: 100vh;
}
.newUI .target {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100vw - 280px - 60px - 25vw - 3rem);
box-sizing: border-box;
padding-right: 1rem;
line-height: 18px;
}
.newUI .applies-to .expander {
margin: 0;
cursor: pointer;
font-size: 3ex;
line-height: .5ex;
vertical-align: super;
letter-spacing: .1ex;
}
.newUI .applies-to:not(.has-more) .expander {
display: none;
}
.newUI .has-favicons .applies-to .expander {
padding-left: 20px;
}
.newUI .target:hover {
background-color: inherit;
}
.newUI .target img {
width: 16px;
height: 16px;
vertical-align: sub;
margin-left: -20px;
margin-right: 4px;
transition: opacity .5s, filter .5s;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(1);
filter: grayscale(1);
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
backface-visibility: hidden;
opacity: .25;
display: none;
}
.newUI .has-favicons .target {
padding-left: 20px;
}
.newUI .has-favicons .target img[src] {
display: inline;
}
.newUI .entry:hover .target img {
opacity: 1;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(0);
filter: grayscale(0);
}
#newUIoptions {
display: none;
}
.newUI #newUIoptions {
display: initial;
}
#newUIoptions > * {
display: flex;
align-items: center;
margin-bottom: auto;
flex-wrap: wrap;
}
#newUIoptions input[type="number"] {
width: 3em;
margin-right: .5em;
}
input[id^="manage.newUI"] {
margin-left: 0;
}
#faviconsHelp {
overflow-y: auto;
font-size: 90%;
padding: 1ex 0 2ex 16px;
}
#faviconsHelp div {
display: flex;
align-items: center;
margin-top: 1ex;
}
/* Default, no update buttons */
.update,
.check-update {
display: none;
}
/* Check update button for things that can*/
.updatable .check-update {
display: inline;
}
/* Update check in progress */
.checking-update .check-update {
display: none;
}
/* Updates available */
.can-update .update {
display: inline;
}
.can-update[data-details$="locally edited"] button.update:after {
content: "*";
}
.can-update .check-update {
display: none;
}
/* Updates not available */
.no-update:not(.update-problem) .check-update {
display: none;
}
/* Updates done */
.update-done .check-update {
display: none;
}
#apply-all-updates:after {
content: " (" attr(data-value) ")";
}
.update-in-progress #check-all-updates {
position: relative;
}
.update-in-progress #update-progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: currentColor;
content: "";
opacity: .35;
}
#update-all-no-updates[data-skipped-edited="true"]:after {
content: " __MSG_updateAllCheckSucceededSomeEdited__";
}
#check-all-updates-force {
margin-top: 1ex;
}
/* highlight updated/added styles */
.highlight {
animation: highlight 10s cubic-bezier(0,.82,.47,.98);
}
@keyframes highlight {
from {
background-color: rgba(128, 128, 128, .5);
}
to {
background-color: none;
}
}
.hidden {
display: none !important;
}
fieldset {
border-width: 1px;
border-radius: 6px;
margin: 1em 0;
}
fieldset > * {
display: flex;
align-items: center;
}
#search {
width: calc(100% - 4px);
margin: 0.25rem 4px 0;
border-radius: 0.25rem;
padding-left: 0.25rem;
border-width: 1px;
}
#import ul {
margin-left: 0;
padding-left: 0;
list-style: none;
}
#import li {
margin-bottom: .5em;
}
#import pre {
background: #eee;
overflow: auto;
margin: 0 0 .5em 0;
}
/* drag-n-drop on import button */
.dropzone:after {
background-color: rgba(0, 0, 0, 0.7);
color: white;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
position: fixed;
padding: calc(50vh - 3em) calc(50vw - 5em);
content: attr(dragndrop-hint);
text-shadow: 1px 1px 10px black;
font-size: xx-large;
text-align: center;
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both;
}
.fadeout.dropzone:after {
animation: fadeout .25s ease-in-out;
animation-fill-mode: both;
}
/* post-import report */
#message-box details:not([data-id="invalid"]) div:hover {
background-color: rgba(128, 128, 128, .3);
}
#message-box details:not(:last-child) {
margin-bottom: 1em;
}
#message-box details small div {
margin-left: 1.5em;
}
.update-history-log {
font-size: 11px;
white-space: pre;
overflow-x: hidden;
text-overflow: ellipsis;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeout {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fadein-25pct {
from {
opacity: 0;
}
to {
opacity: .25;
}
}
@media (max-width: 675px) {
#header {
height: auto;
position: static;
width: auto;
border-right: none;
border-bottom: 1px dashed #AAA;
}
#installed {
position: static;
padding-left: 0;
overflow: visible;
}
#header h1,
#header h2,
#header h3,
#backup-message {
display: none;
}
#header p,
#header fieldset div,
#backup {
display: inline-block;
}
#find-editor-styles {
display: inline-block;
}
#backup {
margin-right: 1em;
}
#backup p,
#header fieldset {
margin: 0;
}
.entry {
margin: 0;
}
}
@media (max-width: 800px) {
body {
flex-direction: column;
}
.newUI #header {
height: auto;
position: static;
width: auto;
border-right: none;
border-bottom: 1px dashed #AAA;
overflow: visible;
}
.newUI #installed {
padding-left: 0;
}
.newUI #header h1,
.newUI #header h2,
.newUI #header h3,
.newUI #header legend,
.newUI #backup-message {
display: none;
}
.newUI #header p,
.newUI #header fieldset div,
.newUI #options,
.newUI #backup,
.newUI #find-editor-styles,
.newUI #header fieldset label,
.newUI #header fieldset input,
.newUI #newUIoptions > * {
display: inline;
vertical-align: middle;
margin-top: 1ex;
margin-bottom: 1ex;
}
.newUI #header > * {
display: inline-block
}
.newUI #header button,
.newUI #header span,
.newUI #header div {
margin-right: 1ex;
}
.newUI #header label,
.newUI #header a {
white-space: nowrap
}
.newUI #backup p,
.newUI #header fieldset {
margin: 0;
padding: 0;
border: none;
}
.newUI #header fieldset input {
margin-left: 0;
}
.newUI #search {
width: auto;
}
.newUI .entry {
margin: 0;
}
.newUI .style-name {
width: 50%;
}
.newUI .target {
max-width: calc(50vw - 60px);
}
}
@media (max-width: 500px) {
.newUI #header > * {
display: inline;
}
.newUI .style-name {
word-break: break-all;
}
}

View File

@ -1,213 +1,33 @@
<html>
<html id="stylus">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title i18n-text="manageTitle"></title>
<style>
body {
margin: 0;
font: 12px arial, sans-serif;
}
a,
a:visited {
color: #555;
-webkit-transition: color 0.5s;
}
a:hover {
color: #999;
}
#header {
height: 100%;
width: 250px;
position: fixed;
top: 0;
padding: 15px;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
}
#header h1 {
margin-top: 0;
}
#installed {
position: relative;
margin-left: 280px;
}
[style-id] {
margin: 10px;
padding: 0 15px;
}
[style-id] {
border-top: 2px solid gray;
}
#installed::after {
content: "";
position: absolute;
top: 0;
width: 100%;
height: 2px;
background-color: #fff;
}
.svg-icon.installed {
cursor: pointer;
vertical-align: middle;
margin-left: 0.3rem;
margin-top: -4px;
transition: fill .5s;
}
a:hover .svg-icon.installed {
fill: hsl(0, 0%, 40%);
}
.style-name {
margin-top: .25em;
word-break: break-word;
}
.applies-to {
word-break: break-word;
}
.applies-to,
.actions {
padding-left: 15px;
}
.applies-to-extra {
font-weight: bold;
}
.disabled h2::after {
content: " (Disabled)";
}
.disabled {
opacity: 0.5;
}
.disabled .disable {
display: none;
}
.enabled .enable {
display: none;
}
.style-name a[target="_blank"] {
text-decoration: none;
}
/* Default, no update buttons */
<link rel="stylesheet" href="manage.css">
<link rel="stylesheet" href="msgbox/msgbox.css">
.update,
.check-update {
display: none;
}
/* Check update button for things that can*/
<style id="style-overrides"></style>
*[style-update-url] .check-update {
display: inline;
}
/* Update check in progress */
.checking-update .check-update {
display: none;
}
/* Updates available */
.can-update .update {
display: inline;
}
.can-update .check-update {
display: none;
}
/* Updates not available */
.no-update .check-update {
display: none;
}
/* Updates done */
.update-done .check-update {
display: none;
}
.hidden {
display: none
}
@media(max-width:675px) {
#header {
height: auto;
position: inherit;
width: auto;
border-right: none;
}
#installed {
margin-left: 0;
}
[style-id] {
margin: 0;
}
}
#header {
overflow: auto;
height: calc(100vh - 30px)
}
fieldset {
border-width: 1px;
border-radius: 6px;
margin: 1em 0;
}
.enabled-only > .disabled,
.edited-only > [style-update-url] {
display: none;
}
#search {
width: calc(100% - 4px);
margin: 0.25rem 4px 0;
border-radius: 0.25rem;
padding-left: 0.25rem;
border-width: 1px;
}
#import ul {
margin-left: 0;
padding-left: 0;
list-style: none;
}
#import li {
margin-bottom: .5em;
}
#import pre {
background: #eee;
overflow: auto;
margin: 0 0 .5em 0;
}
/* drag-n-drop on import button */
.dropzone:after {
background-color: rgba(0, 0, 0, 0.7);
color: white;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
position: fixed;
padding: calc(50vh - 3em) calc(50vw - 5em);
content: attr(dragndrop-hint);
text-shadow: 1px 1px 10px black;
font-size: xx-large;
text-align: center;
animation: fadein 1s cubic-bezier(.03,.67,.08,.94);
animation-fill-mode: both;
}
.fadeout.dropzone:after {
animation: fadeout .25s ease-in-out;
animation-fill-mode: both;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
<!-- Notes:
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
* inter-tag whitespace in templates is automatically removed in localization.js
* i18n-anything attribute automatically creates "anything" attribute
-->
<template data-id="style">
<div>
<h2 class="style-name"></h2>
<p class="applies-to"></p>
<div class="entry">
<h2 class="style-name">
<a class="style-name-link" href="edit.html?id="></a>
<a target="_blank" class="homepage"></a>
</h2>
<p class="applies-to">
<label i18n-text="appliesDisplay"></label>
<span class="targets"></span>
</p>
<p class="actions">
<a class="style-edit-link" href="edit.html?id="><button i18n-text="editStyleLabel"></button></a>
<a class="style-edit-link" href="edit.html?id=">
<button i18n-text="editStyleLabel"></button>
</a>
<button class="enable" i18n-text="enableStyleLabel"></button>
<button class="disable" i18n-text="disableStyleLabel"></button>
<button class="delete" i18n-text="deleteStyleLabel"></button>
@ -218,19 +38,94 @@
</div>
</template>
<template data-id="styleHomepage">
<a target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" class="svg-icon installed" height="16" width="16" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z" id="external-link"></path>
<template data-id="styleCompact">
<div class="entry">
<h2 class="style-name">
<input class="checker" type="checkbox" i18n-title="toggleStyle">
<a class="style-name-link" href="edit.html?id="></a>
</h2>
<p class="actions">
<a target="_blank" class="homepage"></a>
<span i18n-title="deleteStyleLabel">
<svg class="svg-icon delete" viewBox="0 0 20 20">
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
</svg>
</a>
</span>
</p>
<div class="applies-to">
<div class="targets"></div>
<span class="expander">...</span>
</div>
</div>
</template>
<script src="localization.js"></script>
<script src="health.js"></script>
<script src="storage.js"></script>
<template data-id="homepageIconBig">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon shape-rendering="crispEdges" points="3,3 3,17 17,17 17,13 15,13 15,15 5,15 5,5 7,5 7,3 "/>
<polygon points="10,3 12.5,5.5 8,10 10,12 14.5,7.5 17,10 17,3 "/>
</svg>
</template>
<template data-id="homepageIconSmall">
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
</svg>
</template>
<template data-id="updaterIcons">
<span class="updater-icons">
<span class="check-update" i18n-title="checkForUpdate">
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M18,16.6l-3.1-3.1c0.5-0.7,0.9-1.5,1-2.5h-2.1c-0.4,1.7-2,3-3.9,3c-0.8,0-1.6-0.3-2.3-0.7
L10,11H6.1H4.1H4v6l2.3-2.3c1,0.8,2.3,1.3,3.7,1.3c1.3,0,2.5-0.4,3.5-1.1l3.1,3.1L18,16.6z"/>
<path d="M10,6c0.8,0,1.6,0.3,2.3,0.7L10,9h3.9h2.1H16V3l-2.3,2.3C12.7,4.5,11.4,4,10,4
C7,4,4.6,6.2,4.1,9h2.1C6.6,7.3,8.1,6,10,6z"/>
</svg>
</span>
<span class="update" i18n-title="installUpdate">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="16,8 12,8 12,3 8,3 8,8 4,8 10,14 "/>
<rect shape-rendering="crispEdges" x="4" y="15" width="12" height="2"/>
</svg>
</span>
<span class="up-to-date" i18n-title="updateCheckSucceededNoUpdate">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
</svg>
</span>
<span class="updated" i18n-title="updateCompleted">
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
</svg>
</span>
<span class="update-note"></span>
</span>
</template>
<template data-id="appliesToTarget">
<span class="target"></span>
</template>
<template data-id="appliesToSeparator">
<span class="sep">, </span>
</template>
<template data-id="appliesToEverything">
<span class="target" i18n-text="appliesToEverything"></span>
</template>
<template data-id="extraAppliesTo">
<details class="applies-to-extra">
<summary i18n-html="appliesDisplayTruncatedSuffix"></summary>
</details>
</template>
<script src="dom.js"></script>
<script src="messaging.js"></script>
<script src="prefs.js"></script>
<script src="apply.js"></script>
<script src="localization.js"></script>
<script src="manage.js"></script>
</head>
@ -239,24 +134,40 @@
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
<fieldset>
<legend id="filters" i18n-text="manageFilters"></legend>
<div>
<input id="manage.onlyEnabled" type="checkbox">
<label id="manage.onlyEnabled-label" for="manage.onlyEnabled" i18n-text="manageOnlyEnabled"></label>
</div>
<div>
<input id="manage.onlyEdited" type="checkbox">
<label id="manage.onlyEdited-label" for="manage.onlyEdited" i18n-text="manageOnlyEdited"></label>
</div>
<div>
<input id="search" type="search" i18n-placeholder="searchStyles">
</div>
<label>
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<span i18n-text="manageOnlyEnabled"></span>
</label>
<label>
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable)"
data-filter-hide=".updatable">
<span i18n-text="manageOnlyLocal" i18n-title="manageOnlyLocalTooltip"></span>
</label>
<label id="onlyUpdates" class="hidden">
<input type="checkbox"
data-filter=".can-update, .update-problem, .update-done"
data-filter-hide=":not(.updatable):not(.update-done), .no-update:not(.update-problem)">
<span i18n-text="manageOnlyUpdates"></span>
</label>
<input id="search" type="search" i18n-placeholder="searchStyles"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
</fieldset>
<p>
<button id="check-all-updates" i18n-text="checkAllUpdates"></button>
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
<span id="update-history" i18n-title="genericHistoryLabel">
<svg class="svg-icon" viewBox="0 0 20 20" i18n-alt="helpAlt">
<path d="M13,7H7V6h6Zm6,6.5A5.5,5.5,0,0,1,8.61,16H4V3H16V8.61A5.5,5.5,0,0,1,19,13.5ZM8,14c0-.16,0-.84,0-1H7V12H8.21a5.46,5.46,0,0,1,.39-1H7V10H9.26a5.55,5.55,0,0,1,1.09-1H7V8h7V5H6v9Zm10-.5A4.5,4.5,0,1,0,13.5,18,4.5,4.5,0,0,0,18,13.5ZM14,13V10H13v4h4V13Z"/>
</svg>
</span>
</p>
<p>
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
</p>
<p>
<a href="edit.html">
@ -265,23 +176,34 @@
</p>
<div id="options">
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
<label><input id="manage.newUI" type="checkbox"><span i18n-text="manageNewUI"></span></label>
<div id="newUIoptions">
<div>
<input id="show-badge" type="checkbox">
<label id="show-badge-label" for="show-badge" i18n-text="prefShowBadge"></label>
</div>
<input id="manage.newUI.favicons" type="checkbox">
<label for="manage.newUI.favicons" i18n-text="manageFavicons"></label>
<svg class="svg-icon info" viewBox="0 0 14 16" i18n-alt="helpAlt" data-toggle-on-click="#faviconsHelp">
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
</svg>
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
<div>
<input id="popup.stylesFirst" type="checkbox">
<label id="stylesFirst-label" for="popup.stylesFirst" i18n-text="popupStylesFirst"></label>
<input id="manage.newUI.faviconsGray" type="checkbox">
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray"></label>
</div>
</div>
</div>
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
</div>
<div id="more-options">
<h3 id="options-subheading" i18n-text="optionsSubheading"></h3>
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
<button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button>
<p>
<button id="editor-styles-button" i18n-text="editorStylesButton"></button>
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
<button id="manage-shortcuts-button" class="chromium-only"
i18n-text="shortcuts"
i18n-title="shortcutsNote"></button>
<a id="find-editor-styles"
href="https://userstyles.org/styles/browse/chrome-extension"
i18n-title="editorStylesButton"
target="_blank"><button i18n-text="cm_theme"></button></a>
</p>
</div>
</div>
<div id="backup">
<h2 id="backup-title" i18n-text="backupButtons"></h2>
<span id="backup-message" i18n-text="backupMessage"></span>
@ -293,8 +215,9 @@
<p id="manage-text" i18n-html="manageText"></p>
</div>
<div id="installed"></div>
<script src="openOptions.js"></script>
<script src="backup/fileSaveLoad.js"></script>
</body>
<script src="backup/fileSaveLoad.js"></script>
<script src="msgbox/msgbox.js"></script>
</body>
</html>

1229
manage.js

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,25 @@
{
"name": "Stylus",
"version": "1.0.5",
"minimum_chrome_version": "49",
"description": "__MSG_description__",
"homepage_url": "http://add0n.com/stylus.html",
"manifest_version": 2,
"icons": {
"16": "16.png",
"48": "48.png",
"128": "128.png"
"16": "/images/icon/16.png",
"32": "/images/icon/32.png",
"48": "/images/icon/48.png",
"128": "/images/icon/128.png"
},
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"storage",
"*://*/*"
"<all_urls>"
],
"background": {
"scripts": ["messaging.js", "storage-websql.js", "storage.js", "background.js", "update.js"]
"scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"]
},
"commands": {
"openManage": {
@ -32,21 +34,22 @@
"matches": ["<all_urls>"],
"run_at": "document_start",
"all_frames": true,
"match_about_blank": true,
"js": ["apply.js"]
},
{
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
"run_at": "document_end",
"run_at": "document_start",
"all_frames": false,
"js": ["install.js"]
}
],
"browser_action": {
"default_icon": {
"16": "16w.png",
"32": "32w.png",
"19": "19w.png",
"38": "38w.png"
"16": "/images/icon/16w.png",
"32": "/images/icon/32w.png",
"19": "/images/icon/19w.png",
"38": "/images/icon/38w.png"
},
"default_title": "Stylus",
"default_popup": "popup.html"

View File

@ -1,146 +1,328 @@
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
'use strict';
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true;
const OWN_ORIGIN = chrome.runtime.getURL('');
function notifyAllTabs(request) {
const FIREFOX = /Firefox/.test(navigator.userAgent);
const OPERA = /OPR/.test(navigator.userAgent);
const URLS = {
ownOrigin: chrome.runtime.getURL(''),
optionsUI: [
chrome.runtime.getURL('options/index.html'),
'chrome://extensions/?options=' + chrome.runtime.id,
],
configureCommands:
OPERA ? 'opera://settings/configureCommands'
: 'chrome://extensions/configureCommands',
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
chromeWebStore: FIREFOX ? 'N/A' : 'https://chrome.google.com/webstore/',
supported: new RegExp(
'^(file|ftps?|http)://|' +
`^https://${FIREFOX ? '' : '(?!chrome\\.google\\.com/webstore)'}|` +
'^' + chrome.runtime.getURL('')),
};
let BG = chrome.extension.getBackgroundPage();
if (!BG || BG != window) {
document.documentElement.classList.toggle('firefox', FIREFOX);
document.documentElement.classList.toggle('opera', OPERA);
// TODO: remove once our manifest's minimum_chrome_version is 50+
// Chrome 49 doesn't report own extension pages in webNavigation apparently
if (navigator.userAgent.includes('Chrome/49.')) {
getActiveTab().then(BG.updateIcon);
}
}
function notifyAllTabs(msg) {
const originalMessage = msg;
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
// apply/popup/manage use only meta for these two methods,
// editor may need the full code but can fetch it directly,
// so we send just the meta to avoid spamming lots of tabs with huge styles
msg = Object.assign({}, msg, {
style: getStyleWithNoCode(msg.style)
});
}
const affectsAll = !msg.affects || msg.affects.all;
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
const affectsTabs = affectsAll || affectsOwnOriginOnly;
const affectsIcon = affectsAll || msg.affects.icon;
const affectsPopup = affectsAll || msg.affects.popup;
const affectsSelf = affectsPopup || msg.prefs;
if (affectsTabs || affectsIcon) {
// list all tabs including chrome-extension:// which can be ours
chrome.tabs.query({}, tabs => {
for (let tab of tabs) {
if (request.codeIsUpdated !== false || tab.url.startsWith(OWN_ORIGIN)) {
chrome.tabs.sendMessage(tab.id, request);
updateIcon(tab);
chrome.tabs.query(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
for (const tab of tabs) {
// own pages will be notified via runtime.sendMessage later
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))) {
chrome.tabs.sendMessage(tab.id, msg);
}
if (affectsIcon && BG) {
BG.updateIcon(tab);
}
}
});
// notify all open popups
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method});
chrome.runtime.sendMessage(reqPopup);
}
// notify self: the message no longer is sent to the origin in new Chrome
if (typeof applyOnMessage !== 'undefined') {
applyOnMessage(reqPopup);
if (typeof onRuntimeMessage != 'undefined') {
onRuntimeMessage(originalMessage);
}
// notify apply.js on own pages
if (typeof applyOnMessage != 'undefined') {
applyOnMessage(originalMessage);
}
// notify background page and all open popups
if (affectsSelf) {
chrome.runtime.sendMessage(msg);
}
}
function refreshAllTabs() {
function getTab(id) {
return new Promise(resolve =>
chrome.tabs.get(id, tab =>
!chrome.runtime.lastError && resolve(tab)));
}
function getActiveTab() {
return new Promise(resolve =>
chrome.tabs.query({currentWindow: true, active: true}, tabs =>
resolve(tabs[0])));
}
function getActiveTabRealURL() {
return getActiveTab()
.then(getTabRealURL);
}
function getTabRealURL(tab) {
return new Promise(resolve => {
// list all tabs including chrome-extension:// which can be ours
chrome.tabs.query({}, tabs => {
const lastTab = tabs[tabs.length - 1];
for (let tab of tabs) {
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
const message = {method: 'styleReplaceAll', styles};
if (tab.url == location.href && typeof applyOnMessage !== 'undefined') {
applyOnMessage(message);
if (tab.url != 'chrome://newtab/') {
resolve(tab.url);
} else {
chrome.tabs.sendMessage(tab.id, message);
}
updateIcon(tab, styles);
if (tab == lastTab) {
resolve();
}
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
resolve(frame && frame.url || '');
});
}
});
});
}
function updateIcon(tab, styles) {
// while NTP is still loading only process the request for its main frame with a real url
// (but when it's loaded we should process style toggle requests from popups, for example)
if (tab.url == "chrome://newtab/" && tab.status != "complete") {
// opens a tab or activates the already opened one,
// reuses the New Tab page if it's focused now
function openURL({url, currentWindow = true}) {
if (!url.includes('://')) {
url = chrome.runtime.getURL(url);
}
return new Promise(resolve => {
// [some] chromium forks don't handle their fake branded protocols
url = url.replace(/^(opera|vivaldi)/, 'chrome');
// API doesn't handle the hash-fragment part
chrome.tabs.query({url: url.replace(/#.*/, ''), currentWindow}, tabs => {
for (const tab of tabs) {
if (tab.url == url) {
activateTab(tab).then(resolve);
return;
}
if (styles) {
// check for not-yet-existing tabs e.g. omnibox instant search
chrome.tabs.get(tab.id, function() {
if (!chrome.runtime.lastError) {
// for 'styles' asHash:true fake the length by counting numeric ids manually
if (styles.length === undefined) {
styles.length = 0;
for (var id in styles) {
styles.length += id.match(/^\d+$/) ? 1 : 0;
}
}
stylesReceived(styles);
}
});
return;
}
getTabRealURL(tab, function(url) {
// if we have access to this, call directly. a page sending a message to itself doesn't seem to work right.
if (typeof getStyles != "undefined") {
getStyles({matchUrl: url, enabled: true}, stylesReceived);
getActiveTab().then(tab => {
if (tab && tab.url == 'chrome://newtab/') {
chrome.tabs.update({url}, resolve);
} else {
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url, enabled: true}, stylesReceived);
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
}
});
function stylesReceived(styles) {
var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll");
var postfix = disableAll ? "x" : styles.length == 0 ? "w" : "";
chrome.browserAction.setIcon({
path: {
// Material Design 2016 new size is 16px
16: "16" + postfix + ".png", 32: "32" + postfix + ".png",
// Chromium forks or non-chromium browsers may still use the traditional 19px
19: "19" + postfix + ".png", 38: "38" + postfix + ".png",
},
tabId: tab.id
}, function() {
// if the tab was just closed an error may occur,
// e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload
if (!chrome.runtime.lastError) {
var t = prefs.get("show-badge") && styles.length ? ("" + styles.length) : "";
chrome.browserAction.setBadgeText({text: t, tabId: tab.id});
chrome.browserAction.setBadgeBackgroundColor({
color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal')
});
}
});
//console.log("Tab " + tab.id + " (" + tab.url + ") badge text set to '" + t + "'.");
}
}
function getActiveTab(callback) {
chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
callback(tabs[0]);
});
}
function getActiveTabRealURL(callback) {
getActiveTab(function(tab) {
getTabRealURL(tab, callback);
});
function activateTab(tab) {
return Promise.all([
new Promise(resolve => {
chrome.tabs.update(tab.id, {active: true}, resolve);
}),
new Promise(resolve => {
chrome.windows.update(tab.windowId, {focused: true}, resolve);
}),
]);
}
function getTabRealURL(tab, callback) {
if (tab.url != "chrome://newtab/") {
callback(tab.url);
} else {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, function(frame) {
frame && callback(frame.url);
});
}
}
function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags);
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
}
// expands * as .*?
function wildcardAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, "\\$&").replace(/\*/g, '.*?'), flags);
function ignoreChromeError() {
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
}
var configureCommands = {
get url () {
return navigator.userAgent.indexOf('OPR') > -1 ?
'opera://settings/configureCommands' :
'chrome://extensions/configureCommands'
function getStyleWithNoCode(style) {
const stripped = Object.assign({}, style, {sections: []});
for (const section of style.sections) {
stripped.sections.push(Object.assign({}, section, {code: null}));
}
return stripped;
}
// js engine can't optimize the entire function if it contains try-catch
// so we should keep it isolated from normal code in a minimal wrapper
// Update: might get fixed in V8 TurboFan in the future
function tryCatch(func, ...args) {
try {
return func(...args);
} catch (e) {}
}
function tryRegExp(regexp) {
try {
return new RegExp(regexp);
} catch (e) {}
}
function tryJSONparse(jsonString) {
try {
return JSON.parse(jsonString);
} catch (e) {}
}
const debounce = Object.assign((fn, delay, ...args) => {
clearTimeout(debounce.timers.get(fn));
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
}, {
timers: new Map(),
run(fn, ...args) {
debounce.timers.delete(fn);
fn(...args);
},
unregister(fn) {
clearTimeout(debounce.timers.get(fn));
debounce.timers.delete(fn);
},
open: () => {
chrome.tabs.create({
'url': configureCommands.url
});
function deepCopy(obj) {
return obj !== null && obj !== undefined && typeof obj == 'object'
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
: obj;
}
function deepMerge(target, ...args) {
const isArray = typeof target.slice == 'function';
for (const obj of args) {
if (isArray && obj !== null && obj !== undefined) {
for (const element of obj) {
target.push(deepCopy(element));
}
continue;
}
for (const k in obj) {
const value = obj[k];
if (k in target && typeof value == 'object' && value !== null) {
deepMerge(target[k], value);
} else {
target[k] = deepCopy(value);
}
}
}
return target;
}
function sessionStorageHash(name) {
return {
name,
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
set(k, v) {
this.value[k] = v;
this.updateStorage();
},
unset(k) {
delete this.value[k];
this.updateStorage();
},
updateStorage() {
sessionStorage[this.name] = JSON.stringify(this.value);
}
};
}
function onBackgroundReady(...dataPassthru) {
return BG ? Promise.resolve(...dataPassthru) : new Promise(ping);
function ping(resolve) {
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
if (health !== undefined) {
BG = chrome.extension.getBackgroundPage();
resolve(...dataPassthru);
} else {
ping(resolve);
}
});
}
}
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
function getStylesSafe(options) {
return onBackgroundReady(options).then(BG.getStyles);
}
function saveStyleSafe(style) {
return onBackgroundReady(BG.deepCopy(style))
.then(BG.saveStyle)
.then(savedStyle => {
if (style.notify === false) {
handleUpdate(savedStyle, style);
}
return savedStyle;
});
}
function deleteStyleSafe({id, notify = true} = {}) {
return onBackgroundReady({id, notify})
.then(BG.deleteStyle)
.then(() => {
if (!notify) {
handleDelete(id);
}
return id;
});
}
function download(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.timeout = 10e3;
xhr.onloadend = () => (xhr.status == 200
? resolve(xhr.responseText)
: reject(xhr.status));
const [mainUrl, query] = url.split('?');
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(query);
});
}

137
msgbox/msgbox.css Normal file
View File

@ -0,0 +1,137 @@
#message-box {
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
position: fixed;
box-shadow: 5px 5px 50px rgba(0, 0, 0, 0.35);
background-color: rgba(0, 0, 0, .25);
animation: fadein .25s ease-in-out;
z-index: 9999990;
}
#message-box > div {
top: 3rem;
right: 3rem;
min-width: 10rem;
max-width: 50vw;
min-height: 5rem;
max-height: 90vh;
position: fixed;
display: flex;
flex-direction: column;
background-color: white;
box-shadow: 5px 5px 50px rgba(0, 0, 0, 0.35);
z-index: 9999991;
}
#message-box.fadeout {
animation: fadeout .5s ease-in-out;
}
#message-box.center {
align-items: center;
justify-content: center;
}
#message-box.center #message-box-contents {
text-align: center;
}
#message-box.center > div {
top: unset;
right: unset;
}
#message-box-title {
font-weight: bold;
background-color: rgb(145, 208, 198);
padding: .75rem 24px .75rem 52px;
font-size: 1rem;
position: relative;
}
#message-box-title::before {
content: "";
width: 0;
height: 0;
padding: 0 32px 32px 0;
background: url(/images/icon/32.png);
position: absolute;
left: .5rem;
top: 0;
bottom: 0;
margin: auto;
}
#message-box.danger #message-box-title {
background-color: firebrick;
color: white;
}
#message-box.danger #message-box-title::before {
background: url('/images/icon/32x.png');
}
#message-box.danger #message-box-contents {
font-weight: bold;
}
#message-box.danger #message-box-close-icon svg:hover {
fill: #600;
}
#message-box-close-icon {
cursor: pointer;
position: absolute;
right: 3px;
top: 4px;
}
#message-box-close-icon svg {
width: 16px;
height: 16px;
}
#message-box-contents {
overflow: auto;
padding: 1.5rem .75rem;
position: relative;
flex-grow: 9;
word-break: break-word;
}
#message-box-buttons {
padding: .75rem .375rem;
background-color: #f0f0f0;
text-align: center;
}
.non-windows #message-box-buttons {
text-align: right;
direction: rtl;
}
#message-box-buttons button {
margin: 0 .375rem;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeout {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

96
msgbox/msgbox.js Normal file
View File

@ -0,0 +1,96 @@
'use strict';
function messageBox({
title, // [mandatory] the title string for innerHTML
contents, // [mandatory] 1) DOM element 2) string for innerHTML
className = '', // string, CSS class name of the message box element
buttons = [], // array of strings used as labels
onshow, // function(messageboxElement) invoked after the messagebox is shown
blockScroll, // boolean, blocks the page scroll
}) { // RETURNS: Promise resolved to {button[number], enter[boolean], esc[boolean]}
initOwnListeners();
bindGlobalListeners();
createElement();
document.body.appendChild(messageBox.element);
if (onshow) {
onshow(messageBox.element);
}
return new Promise(_resolve => {
messageBox.resolve = _resolve;
});
function initOwnListeners() {
messageBox.listeners = messageBox.listeners || {
closeIcon() {
resolveWith({button: -1});
},
button() {
resolveWith({button: this.buttonIndex});
},
key(event) {
const keyCode = event.keyCode || event.which;
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
&& (keyCode == 13 || keyCode == 27)) {
event.preventDefault();
resolveWith(keyCode == 13 ? {enter: true} : {esc: true});
}
},
scroll() {
scrollTo(blockScroll.x, blockScroll.y);
}
};
}
function resolveWith(value) {
setTimeout(messageBox.resolve, 0, value);
animateElement(messageBox.element, {className: 'fadeout', remove: true})
.then(unbindAndRemoveSelf);
}
function createElement() {
if (messageBox.element) {
unbindAndRemoveSelf();
}
const id = 'message-box';
const putAs = typeof contents == 'string' ? 'innerHTML' : 'appendChild';
messageBox.element = $element({id, className, appendChild: [
$element({appendChild: [
$element({id: `${id}-title`, innerHTML: title}),
$element({id: `${id}-close-icon`, appendChild:
$element({tag: 'SVG#svg', class: 'svg-icon', viewBox: '0 0 20 20', appendChild:
$element({tag: 'SVG#path', d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
})
}),
onclick: messageBox.listeners.closeIcon}),
$element({id: `${id}-contents`, [putAs]: contents}),
$element({id: `${id}-buttons`, appendChild:
buttons.map((textContent, buttonIndex) => textContent &&
$element({
tag: 'button',
buttonIndex,
textContent,
onclick: messageBox.listeners.button,
})
)
}),
]}),
]});
}
function bindGlobalListeners() {
blockScroll = blockScroll && {x: scrollX, y: scrollY};
if (blockScroll) {
window.addEventListener('scroll', messageBox.listeners.scroll);
}
window.addEventListener('keydown', messageBox.listeners.key);
}
function unbindAndRemoveSelf() {
document.removeEventListener('keydown', messageBox.listeners.key);
window.removeEventListener('scroll', messageBox.listeners.scroll);
messageBox.element.remove();
messageBox.element = null;
messageBox.resolve = null;
}
}

View File

@ -1,20 +0,0 @@
/* globals configureCommands */
'use strict';
document.querySelector('#manage-options-button').addEventListener("click", function() {
if (chrome.runtime.openOptionsPage) {
// Supported (Chrome 42+)
chrome.runtime.openOptionsPage();
} else {
// Fallback
window.open(chrome.runtime.getURL('options/index.html'));
}
});
document.querySelector('#manage-shortcuts-button').addEventListener("click", configureCommands.open);
document.querySelector('#editor-styles-button').addEventListener("click", function() {
chrome.tabs.create({
'url': 'https://userstyles.org/styles/browse/chrome-extension'
});
});

View File

@ -1,22 +1,254 @@
body {
margin: 10px;
font-family: "Helvetica Neue",Helvetica,sans-serif;
font-size: 12px;
html.opera {
text-align: center;
}
table {
width: 100%;
html.opera body {
display: inline-block;
text-align: initial;
}
td:last-child {
html.firefox .block {
padding-left: 6px;
}
html.firefox #notes {
padding-left: calc(6px + 2ex);
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, sans-serif;
font-size: 12px;
width: calc(16px + 100px + 8px + 260px + 8px + 60px + 4px + 16px);
}
.firefox .chromium-only {
display: none;
}
.block {
display: flex;
align-items: center;
margin: 1em 0;
border-bottom: 1px dotted #ccc;
padding: 0 0 1em 16px;
position: relative;
}
.block:last-child {
border-bottom: none;
padding-bottom: 0;
}
h1 {
width: 100px;
margin: 0;
font-size: 120%;
font-weight: bold;
padding-right: 8px;
word-wrap: break-word;
}
label {
display: block;
white-space: nowrap;
margin: .25ex 0;
}
label > * {
display: inline-block;
vertical-align: middle;
}
label > :first-child {
width: 260px;
white-space: normal;
margin-right: 8px;
}
label:not([disabled]) > :first-child {
cursor: default;
}
label:not([disabled]):hover > :first-child {
text-shadow: 0 0 0.01px rgba(0, 0, 0, .25);
}
button,
input[type=number],
input[type="color"],
.onoffswitch {
width: 60px;
box-sizing: border-box;
}
a {
text-decoration-skip: ink;
}
button {
text-align: center;
}
input[type=number] {
text-align: right;
}
input[type=number],
button {
width: 80px;
input[type=number]:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
.notes {
input[type="color"] {
box-sizing: border-box;
height: 2em;
}
#actions {
justify-content: space-around;
align-items: stretch;
padding-right: 8px;
}
#actions button {
width: auto;
margin-right: 8px;
}
[data-cmd="check-updates"] button {
position: relative;
}
.update-in-progress [data-cmd="check-updates"] {
opacity: .5;
pointer-events: none;
}
.update-in-progress #update-progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: currentColor;
content: "";
opacity: .35;
}
#updates-installed {
position: absolute;
font-size: 85%;
margin-top: 1px;
}
#updates-installed:after {
content: attr(data-value);
margin-left: .5ex;
font-weight: bold;
}
#updates-installed:not([data-value]),
#updates-installed[data-value=""] {
display: none;
}
#notes {
background-color: #f4f4f4;
padding: 1.5ex 16px 1ex calc(16px + 2ex);
font-size: 90%;
color: #999;
}
#notes ol {
margin: 0;
padding: 0;
}
#notes li:not(last-child) {
margin-bottom: 1ex;
}
#notes a {
color: inherit;
}
#notes a:hover {
color: black;
}
#notes p {
line-height: 1.25;
margin-top: 1ex;
margin-bottom: 1ex;
}
sup {
vertical-align: baseline;
position: relative;
top: -0.4em;
}
@keyframes fadeinout {
0% { opacity: 0 }
10% { opacity: 1 }
25% { opacity: 1 }
100% { opacity: 0 }
}
/* On/Off FlipSwitch https://proto.io/freebies/onoff/ */
.onoffswitch {
position: relative;
margin: 1ex 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.onoffswitch input {
display: none;
}
.onoffswitch span {
display: block;
overflow: hidden;
cursor: pointer;
height: 12px;
padding: 0;
line-height: 12px;
border: 0 solid #E3E3E3;
border-radius: 12px;
background-color: #E0E0E0;
box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1);
}
.onoffswitch span:before {
content: "";
display: block;
width: 18px;
margin: -3px;
background: #efefef;
position: absolute;
top: 0;
bottom: 0;
right: 46px;
border-radius: 18px;
box-shadow: 0 3px 13px 0 rgba(0, 0, 0, 0.4);
}
.onoffswitch input:checked + span {
background-color: #CAEBE3;
}
.onoffswitch input:checked + span, .onoffswitch input:checked + span:before {
border-color: #CAEBE3;
}
.onoffswitch input:checked + span .onoffswitch-inner {
margin-left: 0;
}
.onoffswitch input:checked + span:before {
right: 0;
background-color: #04BA9F;
box-shadow: 3px 6px 18px 0 rgba(0, 0, 0, 0.2);
}

View File

@ -1,62 +1,109 @@
<!DOCTYPE html>
<html>
<html id="stylus">
<head>
<title>Stylus Options</title>
<title i18n-text-append="optionsHeading">Stylus </title>
<link rel="stylesheet" href="index.css">
<script src="../localization.js"></script>
<script src="/dom.js"></script>
<script src="/messaging.js"></script>
<script src="/localization.js"></script>
<script src="/prefs.js"></script>
<script src="/apply.js"></script>
</head>
<body>
<h1 i18n-text="optionsCustomize"></h1>
<table>
<tbody>
<tr>
<td i18n-text="optionsBadgeNormal"></td>
<td><input type="color" id="badgeNormal"></td>
</tr>
<tr>
<td i18n-text="optionsBadgeDisabled"></td>
<td><input type="color" id="badgeDisabled"></td>
</tr>
<tr>
<td i18n-text="optionsPopupWidth"></td>
<td><input type="number" id="popupWidth" min="200"></td>
</tr>
<tr>
<td i18n-text="optionsUpdateInterval"><sup>1</sup></td>
<td><input type="number" min="0" id="updateInterval"></td>
</tr>
</tbody>
</table>
<div>
<button id="save">Save</button>
<span id="status"></span>
<div id="options">
<div class="block">
<h1 i18n-text="optionsCustomizeBadge"></h1>
<div class="items">
<label>
<span i18n-text="prefShowBadge"></span>
<span class="onoffswitch">
<input type="checkbox" id="show-badge">
<span></span>
</span>
</label>
<label>
<span i18n-text="optionsBadgeNormal"></span>
<input type="color" id="badgeNormal">
</label>
<label>
<span i18n-text="optionsBadgeDisabled"></span>
<input type="color" id="badgeDisabled">
</label>
</div>
</div>
<h1 i18n-text="optionsActions"></h1>
<table>
<tbody>
<tr>
<td i18n-text="optionsOpenManager"><sup>2</sup></td>
<td><button type="button" data-cmd="open-manage" i18n-text="optionsOpen"></button></td>
</tr>
<tr>
<td i18n-text="optionsCheckUpdate"></td>
<td>
<span id="update-counter"></span>
<button type="button" data-cmd="check-updates" i18n-text="optionsCheck"></button>
</td>
</tr>
</tbody>
</table>
<div class="notes">
<hr>
1: <span i18n-text="optionsUpdateIntervalNote"></span>
<br>
2: <span i18n-text="optionsOpenManagerNote"></span>, <a href="#" data-cmd="open-keyboard">chrome://extensions/configureCommands</a>
<div class="block">
<h1 i18n-text="optionsCustomizePopup"></h1>
<div class="items">
<label>
<span i18n-text="optionsPopupWidth"></span>
<input type="number" id="popupWidth" min="200" max="800">
</label>
<label>
<span i18n-text="popupStylesFirst"></span>
<span class="onoffswitch">
<input type="checkbox" id="popup.stylesFirst">
<span></span>
</span>
</label>
</div>
</div>
<div class="block">
<h1 i18n-text="optionsCustomizeUpdate"></h1>
<div class="items">
<label>
<span i18n-text="optionsUpdateInterval"><sup>1</sup></span>
<input type="number" min="0" id="updateInterval">
</label>
</div>
</div>
<div class="block">
<h1 i18n-text="optionsAdvanced"></h1>
<div class="items">
<label>
<span i18n-text="optionsAdvancedExposeIframes"> <sup>2</sup></span>
<span class="onoffswitch">
<input type="checkbox" id="exposeIframes">
<span></span>
</span>
</label>
<label>
<span i18n-text="optionsAdvancedContextDelete"></span>
<span class="onoffswitch">
<input type="checkbox" id="editor.contextDelete">
<span></span>
</span>
</label>
</div>
</div>
<div class="block" id="actions">
<button data-cmd="reset" i18n-text="optionsResetButton" i18n-title="optionsReset"></button>
<button data-cmd="open-manage" i18n-text="optionsOpenManager"></button>
<div data-cmd="check-updates">
<button i18n-text="optionsCheck" i18n-title="optionsCheckUpdate">
<span id="update-progress"></span>
</button>
<div id="updates-installed" i18n-text="updatesCurrentlyInstalled"></div>
</div>
<button data-cmd="open-keyboard" class="chromium-only" i18n-text="shortcuts" i18n-title="shortcutsNote"></button>
</div>
</div>
<div id="notes">
<ol>
<li>
<p i18n-text="optionsUpdateIntervalNote"></p>
<p i18n-text="optionsUpdateImportNote"></p>
</li>
<li i18n-text="optionsAdvancedExposeIframesNote"></li>
</ol>
</div>
<script src="/messaging.js"></script>
<script src="index.js"></script>
</body>
</html>

View File

@ -1,97 +1,63 @@
/* globals configureCommands */
'use strict';
function restore () {
chrome.runtime.getBackgroundPage(bg => {
document.getElementById('badgeDisabled').value = bg.prefs.get('badgeDisabled');
document.getElementById('badgeNormal').value = bg.prefs.get('badgeNormal');
document.getElementById('popupWidth').value = localStorage.getItem('popupWidth') || '246';
document.getElementById('updateInterval').value = bg.prefs.get('updateInterval');
});
}
function save () {
chrome.runtime.getBackgroundPage(bg => {
bg.prefs.set('badgeDisabled', document.getElementById('badgeDisabled').value);
bg.prefs.set('badgeNormal', document.getElementById('badgeNormal').value);
localStorage.setItem('popupWidth', document.getElementById('popupWidth').value);
bg.prefs.set(
'updateInterval',
Math.max(0, +document.getElementById('updateInterval').value)
);
// display notification
let status = document.getElementById('status');
status.textContent = 'Options saved.';
setTimeout(() => status.textContent = '', 750);
});
}
document.addEventListener('DOMContentLoaded', restore);
document.getElementById('save').addEventListener('click', save);
setupLivePrefs();
enforceInputRange($('#popupWidth'));
// actions
document.addEventListener('click', e => {
let cmd = e.target.dataset.cmd;
let total = 0, updated = 0;
document.onclick = e => {
const target = e.target.closest('[data-cmd]');
if (!target) {
return;
}
// prevent double-triggering in case a sub-element was clicked
e.stopPropagation();
function update () {
document.getElementById('update-counter').textContent = `${updated}/${total}`;
}
function done (target) {
target.disabled = false;
window.setTimeout(() => {
document.getElementById('update-counter').textContent = '';
}, 750);
}
switch (target.dataset.cmd) {
case 'open-manage':
openURL({url: '/manage.html'});
break;
if (cmd === 'open-manage') {
chrome.tabs.query({
url: chrome.runtime.getURL('manage.html')
}, tabs => {
if (tabs.length) {
chrome.tabs.update(tabs[0].id, {
active: true,
}, () => {
chrome.windows.update(tabs[0].windowId, {
focused: true
});
});
case 'check-updates':
checkUpdates();
break;
case 'open-keyboard':
openURL({url: URLS.configureCommands});
e.preventDefault();
break;
case 'reset':
$$('input')
.filter(input => input.id in prefs.readOnlyValues)
.forEach(input => prefs.reset(input.id));
break;
}
else {
chrome.tabs.create({
url: chrome.runtime.getURL('manage.html')
});
}
});
}
else if (cmd === 'check-updates') {
e.target.disabled = true;
chrome.runtime.getBackgroundPage(bg => {
bg.update.perform((cmd, value) => {
if (cmd === 'count') {
};
function checkUpdates() {
let total = 0;
let checked = 0;
let updated = 0;
const maxWidth = $('#update-progress').parentElement.clientWidth;
BG.updater.checkAllStyles({observer});
function observer(state, value) {
switch (state) {
case BG.updater.COUNT:
total = value;
if (!total) {
done(e.target);
document.body.classList.add('update-in-progress');
break;
case BG.updater.UPDATED:
updated++;
// fallthrough
case BG.updater.SKIPPED:
checked++;
break;
case BG.updater.DONE:
document.body.classList.remove('update-in-progress');
return;
}
$('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
$('#updates-installed').dataset.value = updated || '';
}
}
else if (cmd === 'single-updated' || cmd === 'single-skipped') {
updated += 1;
if (total && updated === total) {
done(e.target);
}
}
update();
});
});
// notify the automatic updater to reset the next automatic update accordingly
chrome.runtime.sendMessage({
method: 'resetInterval'
});
}
else if (cmd === 'open-keyboard') {
configureCommands.open();
}
});
// overwrite the default URL if browser is Opera
document.querySelector('[data-cmd="open-keyboard"]').textContent =
configureCommands.url;

374
popup.css
View File

@ -2,29 +2,52 @@ body {
width: 252px;
font-size: 12px;
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
margin: 0;
}
body > div:not(#installed) {
margin-left: 0.75em;
margin-right: 0.75em;
}
.firefox .chromium-only {
display: none;
}
input[type=checkbox] {
outline: none;
}
#disable-all-wrapper {
padding: 0.3em 0 0.6em;
}
#no-styles {
font-style: italic;
}
#popup-shortcuts-button {
margin-left: 3px;
}
.checker {
display: inline;
}
.style-name {
cursor: default;
font-weight: bold;
display: block;
}
a, a:visited {
color: black;
a {
color: #000;
transition: color .5s;
text-decoration-skip: ink;
}
a:hover {
color: #666;
}
.left-gutter {
@ -32,137 +55,289 @@ a, a:visited {
width: 16px;
vertical-align: top;
}
.left-gutter input {
margin-bottom: 1px;
margin-top: 0;
margin-left: 0;
}
.main-controls {
display: table-cell;
}
.entry {
padding: 0.5em 0;
}
.entry:first-child {
padding-top: 0;
}
#unavailable,
#installed {
border-bottom: 1px solid black;
padding-bottom: 2px;
}
body > DIV:last-of-type,
body.blocked > DIV {
border-bottom: none;
}
#installed {
padding-top: 2px;
max-height: 434px;
overflow-y: auto;
}
#installed.disabled .style-name {
text-decoration: line-through;
}
#installed .actions {
cursor: default;
}
#installed .actions a {
cursor: pointer;
text-decoration: none;
}
#installed .style-edit-link {
/* entry */
.entry {
display: flex;
align-items: center;
padding: 5px 0.75em;
}
.entry:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
.entry .style-edit-link {
margin-right: 2px;
}
#installed .style-edit-link, #installed .delete {
.entry .style-edit-link,
.entry .delete {
display: inline-block;
padding: 0 1px 0;
}
.entry .main-controls {
display: flex;
flex: 1;
width: calc(100% - 20px);
align-items: center;
}
.entry .main-controls label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
.not-applied .checker,
.not-applied .style-name,
.not-applied .actions > * {
opacity: .2;
transition: opacity .5s ease-in-out .25s, color .5s ease-in-out .25s;
}
.not-applied:hover .checker,
.not-applied:hover .style-name,
.not-applied:hover .actions > * {
opacity: 1;
}
.not-applied:hover .style-name {
color: darkred;
}
.regexp-problem-indicator {
background-color: #d00;
width: 14px;
height: 14px;
line-height: 15px;
border-radius: 8px;
margin-right: 6px;
margin-left: 6px;
text-align: center;
color: white;
font-weight: bold;
box-sizing: border-box;
cursor: pointer;
font-size: 90%;
}
.regexp-partial .actions,
.regexp-invalid .actions {
order: 2;
}
#regexp-explanation {
position: fixed;
background-color: white;
left: 0;
right: 0;
padding: .5rem;
font-size: 90%;
border-top: 2px solid black;
border-bottom: 2px solid black;
box-shadow: 0 0 100px black;
display: flex;
flex-direction: column;
z-index: 999999;
}
#regexp-explanation > div {
display: none;
list-style-type: none;
padding: 0;
margin: 0;
}
.regexp-partial #regexp-partial,
.regexp-invalid #regexp-invalid {
display: block;
}
#regexp-explanation > div:not(:last-child) {
margin-bottom: .5rem;
}
.svg-icon {
pointer-events: none;
transition: fill .5s;
width: 14px;
height: 16px;
fill: #666;
}
a:hover .svg-icon {
fill: #000000;
fill: #000;
}
body > .actions {
margin-top: 0.5em;
}
.actions > div:not(:last-child):not(#disable-all-wrapper), .actions > .main-controls > div:not(:last-child), #unavailable:not(:last-child), #unavailable + .actions {
.actions > div:not(:last-child):not(#disable-all-wrapper),
.actions > .main-controls > div:not(:last-child) {
margin-bottom: 0.75em;
}
.actions input, .actions label {
.actions input,
.actions label {
vertical-align: middle;
}
#unavailable {
border: none;
display: none; /* flex */
align-items: center;
justify-content: center;
font-size: 14px;
}
body.blocked #installed,
body.blocked #find-styles,
body.blocked #write-style,
body:not(.blocked) #unavailable {
body.blocked #installed > *,
body.blocked .actions > .main-controls,
body.blocked .actions > .left-gutter {
display: none;
}
/* Never shown, but can be enabled with a style */
.enable, .disable {
.enable,
.disable {
display: none;
}
/* 'New style' links */
#write-style-for {margin-right: .6ex}
.write-style-link {margin-left: .6ex}
.write-style-link::before, .write-style-link::after {font-size: x-small}
.write-style-link::before {content: "\00ad"} /* "soft" hyphen */
#match {overflow-wrap: break-word;}
#write-style {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#write-style-for {
margin-right: .6ex
}
.write-style-link {
margin-left: .6ex
}
.write-style-link::before,
.write-style-link::after {
font-size: 12px
}
.write-style-link::before {
content: "\00ad"; /* "soft" hyphen */
}
#match {
overflow-wrap: break-word;
display: block;
flex-grow: 9;
min-width: 200px;
}
/* "breadcrumbs" 'new style' links */
.breadcrumbs > .write-style-link {margin-left: 0}
.breadcrumbs:hover a {color: #bbb; text-decoration: none}
.breadcrumbs > .write-style-link {
margin-left: 0
}
.breadcrumbs:hover a {
color: #bbb;
text-decoration: none
}
/* use just the subdomain name instead of the full domain name */
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {font-size: 0}
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {
font-size: 0
}
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2))::before {
content: attr(subdomain);
}
/* "dot" after each subdomain name */
.breadcrumbs > .write-style-link[subdomain]::after {content: "."}
.breadcrumbs > .write-style-link[subdomain]::after {
content: "."
}
/* no "dot" after top-level domain */
.breadcrumbs > .write-style-link:nth-last-child(2)::after {content: none}
.breadcrumbs > .write-style-link:nth-last-child(2)::after {
content: none
}
/* "forward slash" before path ("this URL") */
.breadcrumbs > .write-style-link:last-child::before {content: "\200b/"}
.breadcrumbs > .write-style-link:last-child::before {
content: "\200b/"
}
.breadcrumbs > .write-style-link:last-child:first-child::before,
.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {content: none}
.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {
content: none
}
/* suppress TLD-only link */
.breadcrumbs > .write-style-link[subdomain=""] {display: none}
.breadcrumbs > .write-style-link[subdomain=""] {
display: none
}
/* :hover style */
.breadcrumbs.url\(\) > .write-style-link, /* :hover or :focus on "this URL" sets class="url()" */
.breadcrumbs.url\(\) > .write-style-link,
/* :hover or :focus on "this URL" sets class="url()" */
.breadcrumbs > .write-style-link:hover,
.breadcrumbs > .write-style-link:focus,
.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain],
.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] {
color: inherit;
text-decoration: underline;
text-decoration-skip: ink;
}
/* action buttons */
#popup-options {
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 1.2em 0;
}
#popup-options button {
margin: 0 2px;
width: 33%;
@ -171,50 +346,17 @@ body:not(.blocked) #unavailable {
text-overflow: ellipsis;
}
/* margins */
body {
margin: 0;
}
body>div:not(#installed) {
margin-left:0.75em;
margin-right:0.75em;
}
#unavailable {
margin-top: 0.75em;
}
#installed .entry {
}
/* entries */
#installed .entry {
display: flex;
align-items: center;
padding: 5px 0.75em;
}
#installed .entry:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
#installed .main-controls {
display: flex;
flex: 1;
width: calc(100% - 20px);
align-items: center;
}
#installed .main-controls label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
/* confirm */
#confirm,
#confirm > div > span {
align-items: center;
justify-content: center;
}
#confirm {
z-index: 2147483647;
display: none; /* flex */
display: none;
position: absolute;
left: 0;
top: 0;
@ -223,31 +365,107 @@ body>div:not(#installed) {
margin: 0 !important;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.4);
animation: lights-off .5s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both;
}
#confirm.lights-on {
animation: lights-on .25s ease-in-out;
animation-fill-mode: both;
}
#confirm.lights-on > div {
display: none;
}
#confirm[data-display=true] {
display: flex;
}
#confirm > div {
width: 80%;
height: 100px;
max-height: 80%;
min-height: 8em;
background-color: #fff;
display: flex;
flex-direction: column;
border: solid 2px rgba(0, 0, 0, 0.5);
}
#confirm > div > span {
display: flex;
flex: 1;
padding: 0 10px;
}
#confirm > div > b {
padding: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#confirm > div > div {
padding: 10px;
direction: rtl;
text-align: center;
}
.non-windows #confirm > div > div {
direction: rtl;
text-align: right;
}
.unreachable .entry {
opacity: .25;
}
.blocked:before,
.unreachable:before {
padding: 5px 0.75em;
display: block;
font-weight: bold;
}
.blocked #installed:before,
.unreachable #installed:before {
padding: 1px 0.75em 9px;
display: block;
font-size: 90%;
margin-bottom: 5px;
}
.blocked:before {
content: "__MSG_stylusUnavailableForURL__";
}
.blocked #installed:before {
content: "__MSG_stylusUnavailableForURLdetails__";
}
.unreachable:before {
content: "__MSG_unreachableContentScript__";
}
.unreachable #installed:before {
content: "__MSG_unreachableFileHint__";
border-bottom: 1px solid black;
}
@keyframes lights-off {
from {
background-color: transparent;
}
to {
background-color: rgba(0, 0, 0, 0.4);
}
}
@keyframes lights-on {
from {
background-color: rgba(0, 0, 0, 0.4);
}
to {
background-color: transparent;
}
}

View File

@ -1,11 +1,17 @@
<html>
<html id="stylus">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="stylesheet" href="popup.css">
<!-- Notes:
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
* inter-tag whitespace in templates is automatically removed in localization.js
* i18n-anything attribute automatically creates "anything" attribute
-->
<template data-id="style">
<div>
<div class="entry">
<div class="left-gutter">
<input class="checker" type="checkbox">
</div>
@ -14,14 +20,14 @@
<div class="actions">
<a href="#" class="enable" i18n-text="enableStyleLabel"></a>
<a href="#" class="disable" i18n-text="disableStyleLabel"></a>
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel"> <!--`i18n-title` automatically creates `title` attribute -->
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-icon edit" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16">
<path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"></path>
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel">
<svg class="svg-icon edit" viewBox="0 0 14 16">
<path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"/>
</svg>
</a>
<a href="#" class="delete" i18n-title="deleteStyleLabel">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-icon remove" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16">
<path fill-rule="evenodd" d="M11 2H9c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1H2c-.55 0-1 .45-1 1v1c0 .55.45 1 1 1v9c0 .55.45 1 1 1h7c.55 0 1-.45 1-1V5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 12H3V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9zm1-10H2V3h9v1z"></path>
<svg class="svg-icon remove" viewBox="0 0 14 16">
<path fill-rule="evenodd" d="M11 2H9c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1H2c-.55 0-1 .45-1 1v1c0 .55.45 1 1 1v9c0 .55.45 1 1 1h7c.55 0 1-.45 1-1V5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 12H3V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9zm1-10H2V3h9v1z"/>
</svg>
</a>
</div>
@ -29,31 +35,49 @@
</div>
</template>
<script src="localization.js"></script>
<script src="health.js"></script>
<script src="storage.js"></script>
<template data-id="writeStyle">
<a class="write-style-link"></a>
</template>
<template data-id="noStyles">
<div id="no-styles" class="entry" i18n-text="noStylesForSite"></div>
</template>
<template data-id="regexpProblemIndicator">
<div class="regexp-problem-indicator" i18n-title="styleRegexpProblemTooltip"></div>
</template>
<template data-id="regexpProblemExplanation">
<div id="regexp-explanation">
<div id="regexp-partial" i18n-html="styleRegexpPartialExplanation"></div>
<div id="regexp-invalid" i18n-text="styleRegexpInvalidExplanation"></div>
<button i18n-text="confirmOK"></button>
</div>
</template>
<script src="dom.js"></script>
<script src="messaging.js"></script>
<script src="localization.js"></script>
<script src="prefs.js"></script>
<script src="apply.js"></script>
<script src="popup.js"></script>
</head>
<body id="stylus-popup">
<!-- confirm -->
<div id="confirm">
<div>
<b>Style's Name</b>
<span i18n-text="deleteStyleConfirm"></span>
<div>
<input type="button" i18n-value="confirmCancel" data-cmd="cancel">
<input type="button" i18n-value="confirmOK" data-cmd="ok">
<button i18n-text="confirmDelete" data-cmd="ok"></button>
<button i18n-text="confirmCancel" data-cmd="cancel"></button>
</div>
</div>
</div>
<div id="unavailable">
<div class="main-controls"><span id="unavailable-message" i18n-text="stylishUnavailableForURL"></span>
</div>
</div>
<div id="installed"></div>
<div class="actions">
<div id="disable-all-wrapper">
<div class="left-gutter">
@ -69,17 +93,17 @@
<a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a>
</div>
<div id="write-style">
<span id="write-style-for" i18n-text="writeStyleFor"><br></span>
<span id="write-style-for" i18n-text="writeStyleFor"></span>
</div>
</div>
<!-- Actions -->
<div id="popup-options">
<button id="popup-manage-button" i18n-text="openManage"></button>
<button id="popup-options-button" i18n-text="openOptionsPopup">
<button id="popup-shortcuts-button" i18n-text="openShortcutsPopup"></button>
<button id="popup-manage-button" i18n-text="openManage" data-href="manage.html"></button>
<button id="popup-options-button" i18n-text="openOptionsPopup"></button>
<button id="popup-shortcuts-button" class="chromium-only"
i18n-text="shortcuts"
i18n-title="shortcutsNote"></button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

569
popup.js
View File

@ -1,246 +1,407 @@
/* globals configureCommands */
'use strict';
var writeStyleTemplate = document.createElement("a");
writeStyleTemplate.className = "write-style-link";
let installed;
let tabURL;
const handleEvent = {};
var installed = document.getElementById("installed");
if (!prefs.get("popup.stylesFirst")) {
document.body.insertBefore(document.querySelector("body > .actions"), installed);
}
getActiveTabRealURL(updatePopUp);
function updatePopUp(url) {
var urlWillWork = /^(file|http|https|ftps?|chrome\-extension):/.exec(url);
if (!urlWillWork) {
document.body.classList.add("blocked");
document.getElementById("unavailable").style.display = "flex";
return;
}
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url}, showStyles);
document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url);
// Write new style links
var writeStyleLinks = [],
container = document.createElement('span');
container.id = "match";
// For this URL
var urlLink = writeStyleTemplate.cloneNode(true);
urlLink.href = "edit.html?url-prefix=" + encodeURIComponent(url);
urlLink.appendChild(document.createTextNode( // switchable; default="this&nbsp;URL"
!prefs.get("popup.breadcrumbs.usePath")
? t("writeStyleForURL").replace(/ /g, "\u00a0")
: /\/\/[^/]+\/(.*)/.exec(url)[1]
));
urlLink.title = "url-prefix(\"$\")".replace("$", url);
writeStyleLinks.push(urlLink);
document.querySelector("#write-style").appendChild(urlLink)
if (prefs.get("popup.breadcrumbs")) { // switchable; default=enabled
urlLink.addEventListener("mouseenter", function(event) { this.parentNode.classList.add("url()") }, false);
urlLink.addEventListener("focus", function(event) { this.parentNode.classList.add("url()") }, false);
urlLink.addEventListener("mouseleave", function(event) { this.parentNode.classList.remove("url()") }, false);
urlLink.addEventListener("blur", function(event) { this.parentNode.classList.remove("url()") }, false);
}
// For domain
var domains = getDomains(url)
domains.forEach(function(domain) {
// Don't include TLD
if (domains.length > 1 && domain.indexOf(".") == -1) {
return;
}
var domainLink = writeStyleTemplate.cloneNode(true);
domainLink.href = "edit.html?domain=" + encodeURIComponent(domain);
domainLink.appendChild(document.createTextNode(domain));
domainLink.title = "domain(\"$\")".replace("$", domain);
domainLink.setAttribute("subdomain", domain.substring(0, domain.indexOf(".")));
writeStyleLinks.push(domainLink);
getActiveTabRealURL().then(url => {
tabURL = URLS.supported.test(url) ? url : '';
Promise.all([
tabURL && getStylesSafe({matchUrl: tabURL}),
onDOMready().then(() => {
initPopup(tabURL);
}),
]).then(([styles]) => {
showStyles(styles);
});
});
var writeStyle = document.querySelector("#write-style");
writeStyleLinks.forEach(function(link, index) {
link.addEventListener("click", openLinkInTabOrWindow, false);
container.appendChild(link);
});
if (prefs.get("popup.breadcrumbs")) {
container.classList.add("breadcrumbs");
container.appendChild(container.removeChild(container.firstChild));
chrome.runtime.onMessage.addListener(onRuntimeMessage);
function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleAdded':
case 'styleUpdated':
handleUpdate(msg.style);
break;
case 'styleDeleted':
handleDelete(msg.id);
break;
case 'prefChanged':
if ('popup.stylesFirst' in msg.prefs) {
const stylesFirst = msg.prefs['popup.stylesFirst'];
const actions = $('body > .actions');
const before = stylesFirst ? actions : actions.nextSibling;
document.body.insertBefore(installed, before);
} else if ('popupWidth' in msg.prefs) {
setPopupWidth(msg.prefs.popupWidth);
}
break;
}
writeStyle.appendChild(container);
}
function showStyles(styles) {
var enabledFirst = prefs.get("popup.enabledFirst");
styles.sort(function(a, b) {
if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1;
return a.name.localeCompare(b.name);
});
if (styles.length == 0) {
installed.innerHTML = "<div class='entry' id='no-styles'>" + t('noStylesForSite') + "</div>";
function setPopupWidth(width = prefs.get('popupWidth')) {
document.body.style.width =
Math.max(200, Math.min(800, width)) + 'px';
}
styles.map(createStyleElement).forEach(function(e) {
installed.appendChild(e);
});
function initPopup(url) {
installed = $('#installed');
setPopupWidth();
// force Chrome to resize the popup
if (!FIREFOX) {
document.body.style.height = '10px';
document.documentElement.style.height = '10px';
}
function createStyleElement(style) {
var e = template.style.cloneNode(true);
var checkbox = e.querySelector(".checker");
checkbox.id = "style-" + style.id;
checkbox.checked = style.enabled;
// action buttons
$('#disableAll').onchange = function() {
installed.classList.toggle('disabled', this.checked);
};
setupLivePrefs();
e.setAttribute("class", "entry " + (style.enabled ? "enabled" : "disabled"));
e.setAttribute("style-id", style.id);
var styleName = e.querySelector(".style-name");
styleName.appendChild(document.createTextNode(style.name));
styleName.setAttribute("for", "style-" + style.id);
$('#find-styles-link').onclick = handleEvent.openURLandHide;
$('#popup-manage-button').onclick = handleEvent.openURLandHide;
$('#popup-options-button').onclick = () => {
chrome.runtime.openOptionsPage();
window.close();
};
const shortcutsButton = $('#popup-shortcuts-button');
shortcutsButton.dataset.href = URLS.configureCommands;
shortcutsButton.onclick = handleEvent.openURLandHide;
if (!prefs.get('popup.stylesFirst')) {
document.body.insertBefore(
$('body > .actions'),
installed);
}
// find styles link
$('#find-styles a').href =
'https://userstyles.org/styles/browse/all/' +
encodeURIComponent(url.startsWith('file:') ? 'file:' : url);
if (!url) {
document.body.classList.add('blocked');
return;
}
getActiveTab().then(tab => {
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => {
if (pong === undefined) {
document.body.classList.add('unreachable');
}
});
});
// Write new style links
const writeStyle = $('#write-style');
const matchTargets = document.createElement('span');
const matchWrapper = document.createElement('span');
matchWrapper.id = 'match';
matchWrapper.appendChild(matchTargets);
// For this URL
const urlLink = template.writeStyle.cloneNode(true);
Object.assign(urlLink, {
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
title: `url-prefix("${url}")`,
textContent: prefs.get('popup.breadcrumbs.usePath')
? new URL(url).pathname.slice(1)
: t('writeStyleForURL').replace(/ /g, '\u00a0'), // this&nbsp;URL
onclick: handleEvent.openLink,
});
if (prefs.get('popup.breadcrumbs')) {
urlLink.onmouseenter =
urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
urlLink.onmouseleave =
urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
}
matchTargets.appendChild(urlLink);
// For domain
const domains = BG.getDomains(url);
for (const domain of domains) {
// Don't include TLD
if (domains.length > 1 && !domain.includes('.')) {
continue;
}
const domainLink = template.writeStyle.cloneNode(true);
Object.assign(domainLink, {
href: 'edit.html?domain=' + encodeURIComponent(domain),
textContent: domain,
title: `domain("${domain}")`,
onclick: handleEvent.openLink,
});
domainLink.setAttribute('subdomain', domain.substring(0, domain.indexOf('.')));
matchTargets.appendChild(domainLink);
}
if (prefs.get('popup.breadcrumbs')) {
matchTargets.classList.add('breadcrumbs');
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
}
writeStyle.appendChild(matchWrapper);
}
function showStyles(styles) {
if (!styles) {
return;
}
if (!styles.length) {
installed.innerHTML = template.noStyles.outerHTML;
return;
}
const enabledFirst = prefs.get('popup.enabledFirst');
styles.sort((a, b) => (
enabledFirst && a.enabled !== b.enabled
? !(a.enabled < b.enabled) ? -1 : 1
: a.name.localeCompare(b.name)
));
let postponeDetect = false;
const t0 = performance.now();
const container = document.createDocumentFragment();
for (const style of styles) {
createStyleElement({style, container, postponeDetect});
postponeDetect = postponeDetect || performance.now() - t0 > 100;
}
installed.appendChild(container);
getStylesSafe({matchUrl: tabURL, strictRegexp: false})
.then(unscreenedStyles => {
for (const unscreened of unscreenedStyles) {
if (!styles.includes(unscreened)) {
postponeDetect = postponeDetect || performance.now() - t0 > 100;
createStyleElement({
style: Object.assign({appliedSections: [], postponeDetect}, unscreened),
});
}
}
});
}
function createStyleElement({
style,
container = installed,
postponeDetect,
}) {
const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
Object.assign(entry, {
id: 'style-' + style.id,
styleId: style.id,
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
onmousedown: handleEvent.maybeEdit,
});
const checkbox = $('.checker', entry);
Object.assign(checkbox, {
id: 'style-' + style.id,
checked: style.enabled,
onclick: handleEvent.toggle,
});
const editLink = $('.style-edit-link', entry);
Object.assign(editLink, {
href: editLink.getAttribute('href') + style.id,
onclick: handleEvent.openLink,
});
const styleName = $('.style-name', entry);
Object.assign(styleName, {
htmlFor: 'style-' + style.id,
onclick: handleEvent.name,
});
styleName.checkbox = checkbox;
var editLink = e.querySelector(".style-edit-link");
editLink.setAttribute("href", editLink.getAttribute("href") + style.id);
editLink.addEventListener("click", openLinkInTabOrWindow, false);
styleName.appendChild(document.createTextNode(style.name));
styleName.addEventListener("click", function() { this.checkbox.click(); event.preventDefault(); });
// clicking the checkbox will toggle it, and this will run after that happens
checkbox.addEventListener("click", function() { enable(event, event.target.checked); }, false);
e.querySelector(".enable").addEventListener("click", function() { enable(event, true); }, false);
e.querySelector(".disable").addEventListener("click", function() { enable(event, false); }, false);
$('.enable', entry).onclick = handleEvent.toggle;
$('.disable', entry).onclick = handleEvent.toggle;
$('.delete', entry).onclick = handleEvent.delete;
e.querySelector(".delete").addEventListener("click", function() { doDelete(event, false); }, false);
return e;
if (postponeDetect) {
setTimeout(detectSloppyRegexps, 0, {entry, style});
} else {
detectSloppyRegexps({entry, style});
}
function enable(event, enabled) {
var id = getId(event);
enableStyle(id, enabled);
const oldElement = $('#style-' + style.id);
if (oldElement) {
oldElement.parentNode.replaceChild(entry, oldElement);
} else {
container.appendChild(entry);
}
}
function doDelete() {
document.getElementById('confirm').dataset.display = true;
let id = getId(event);
document.querySelector('#confirm b').textContent =
document.querySelector(`[style-id="${id}"] label`).textContent;
document.getElementById('confirm').dataset.id = id;
Object.assign(handleEvent, {
getClickedStyleId(event) {
return (handleEvent.getClickedStyleElement(event) || {}).styleId;
},
getClickedStyleElement(event) {
return event.target.closest('.entry');
},
name(event) {
this.checkbox.click();
event.preventDefault();
},
toggle(event) {
saveStyleSafe({
id: handleEvent.getClickedStyleId(event),
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
});
},
delete(event) {
const id = handleEvent.getClickedStyleId(event);
const box = $('#confirm');
box.dataset.display = true;
box.style.cssText = '';
$('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name;
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
window.onkeydown = event => {
const keyCode = event.keyCode || event.which;
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
&& (keyCode == 13 || keyCode == 27)) {
event.preventDefault();
confirm(keyCode == 13);
}
document.getElementById('confirm').addEventListener('click', e => {
let cmd = e.target.dataset.cmd;
if (cmd === 'ok') {
deleteStyle(document.getElementById('confirm').dataset.id, () => {
};
function confirm(ok) {
window.onkeydown = null;
animateElement(box, {className: 'lights-on'})
.then(() => (box.dataset.display = false));
if (ok) {
deleteStyleSafe({id}).then(() => {
// update view with 'No styles installed for this site' message
if (document.getElementById('installed').children.length === 0) {
if (!installed.children.length) {
showStyles([]);
}
});
}
//
if (cmd) {
document.getElementById('confirm').dataset.display = false;
}
},
indicator(event) {
const entry = handleEvent.getClickedStyleElement(event);
const info = template.regexpProblemExplanation.cloneNode(true);
$$('#' + info.id).forEach(el => el.remove());
$$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
$$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
entry.appendChild(info);
},
closeExplanation() {
$('#regexp-explanation').remove();
},
openLink(event) {
if (!prefs.get('openEditInWindow', false)) {
handleEvent.openURLandHide.call(this, event);
return;
}
event.preventDefault();
chrome.windows.create(
Object.assign({
url: this.href
}, prefs.get('windowPosition', {}))
);
close();
},
maybeEdit(event) {
if (!(
event.button == 0 && (event.ctrlKey || event.metaKey) ||
event.button == 1 ||
event.button == 2)) {
return;
}
// open an editor on middleclick
if (event.target.matches('.entry, .style-name, .style-edit-link')) {
this.onmouseup = () => $('.style-edit-link', this).click();
this.oncontextmenu = event => event.preventDefault();
event.preventDefault();
return;
}
// prevent the popup being opened in a background tab
// when an irrelevant link was accidentally clicked
if (event.target.closest('a')) {
event.preventDefault();
return;
}
},
openURLandHide(event) {
event.preventDefault();
openURL({url: this.href || this.dataset.href})
.then(window.close);
},
});
function getBrowser() {
if (navigator.userAgent.indexOf("OPR") > -1) {
return "Opera";
}
return "Chrome";
}
function getId(event) {
var e = event.target;
while (e) {
if (e.hasAttribute("style-id")) {
return e.getAttribute("style-id");
}
e = e.parentNode;
}
return null;
}
function openLinkInTabOrWindow(event) {
event.preventDefault();
if (prefs.get("openEditInWindow", false)) {
var options = {url: event.target.href}
var wp = prefs.get("windowPosition", {});
for (var k in wp) options[k] = wp[k];
chrome.windows.create(options);
} else {
openLink(event);
}
close();
}
function openLink(event) {
event.preventDefault();
chrome.runtime.sendMessage({method: "openURL", url: event.target.href});
close();
}
function handleUpdate(style) {
var styleElement = installed.querySelector("[style-id='" + style.id + "']");
if (styleElement) {
installed.replaceChild(createStyleElement(style), styleElement);
} else {
getActiveTabRealURL(function(url) {
if (chrome.extension.getBackgroundPage().getApplicableSections(style, url).length) {
// a new style for the current url is installed
document.getElementById("unavailable").style.display = "none";
installed.appendChild(createStyleElement(style));
if ($('#style-' + style.id)) {
createStyleElement({style});
return;
}
});
// Add an entry when a new style for the current url is installed
if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
document.body.classList.remove('blocked');
createStyleElement({style});
}
}
function handleDelete(id) {
var styleElement = installed.querySelector("[style-id='" + id + "']");
if (styleElement) {
installed.removeChild(styleElement);
}
$$('#style-' + id).forEach(el => el.remove());
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.method == "updatePopup") {
switch (request.reason) {
case "styleAdded":
case "styleUpdated":
handleUpdate(request.style);
break;
case "styleDeleted":
handleDelete(request.id);
break;
/*
According to CSS4 @document specification the entire URL must match.
Stylish-for-Chrome implemented it incorrectly since the very beginning.
We'll detect styles that abuse the bug by finding the sections that
would have been applied by Stylish but not by us as we follow the spec.
Additionally we'll check for invalid regexps.
*/
function detectSloppyRegexps({entry, style}) {
const {
appliedSections =
BG.getApplicableSections({style, matchUrl: tabURL}),
wannabeSections =
BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
} = style;
BG.compileStyleRegExps({style, compileAll: true});
entry.hasInvalidRegexps = wannabeSections.some(section =>
section.regexps.some(rx => !BG.cachedStyles.regexps.has(rx)));
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
if (!appliedSections.length) {
entry.classList.add('not-applied');
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
}
if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
const indicator = template.regexpProblemIndicator.cloneNode(true);
indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
indicator.onclick = handleEvent.indicator;
$('.main-controls', entry).appendChild(indicator);
}
}
});
["find-styles-link"].forEach(function(id) {
document.getElementById(id).addEventListener("click", openLink, false);
});
document.getElementById("disableAll").addEventListener("change", function(event) {
installed.classList.toggle("disabled", prefs.get("disableAll"));
});
setupLivePrefs(["disableAll"]);
document.querySelector('#popup-manage-button').addEventListener("click", function() {
window.open(chrome.runtime.getURL('manage.html'));
});
document.querySelector('#popup-options-button').addEventListener("click", function() {
if (chrome.runtime.openOptionsPage) {
// Supported (Chrome 42+)
chrome.runtime.openOptionsPage();
} else {
// Fallback
window.open(chrome.runtime.getURL('options/index.html'));
}
});
document.querySelector('#popup-shortcuts-button').addEventListener("click", configureCommands.open);
// popup width
document.body.style.width = (localStorage.getItem('popupWidth') || '246') + 'px';

352
prefs.js Normal file
View File

@ -0,0 +1,352 @@
/* global prefs: true, contextMenus */
'use strict';
// eslint-disable-next-line no-var
var prefs = new function Prefs() {
const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window
'windowPosition': {}, // detached window position
'show-badge': true, // display text on popup menu icon
'disableAll': false, // boss key
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
'popup.enabledFirst': true, // display enabled styles before disabled styles
'popup.stylesFirst': true, // display enabled styles before disabled styles
'manage.onlyEnabled': false, // display only enabled styles
'manage.onlyLocal': false, // display only styles created locally
'manage.newUI': true, // use the new compact layout
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
'manage.newUI.faviconsGray': true, // gray out favicons
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
'editor.options': {}, // CodeMirror.defaults.*
'editor.lineWrapping': true, // word wrap
'editor.smartIndent': true, // 'smart' indent
'editor.indentWithTabs': false, // smart indent with tabs
'editor.tabSize': 4, // tab width, in spaces
'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
'editor.theme': 'default', // CSS theme
'editor.beautify': { // CSS beautifier
selector_separator_newline: true,
newline_before_open_brace: false,
newline_after_open_brace: true,
newline_between_properties: true,
newline_before_close_brace: true,
newline_between_rules: false,
end_with_newline: false,
space_around_selector_separator: true,
},
'editor.lintDelay': 500, // lint gutter marker update delay, ms
'editor.lintReportDelay': 4500, // lint report update delay, ms
'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
// selection = only when something is selected
// '' (empty string) = disabled
'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu
'badgeDisabled': '#8B0000', // badge background color when disabled
'badgeNormal': '#006666', // badge background color
'popupWidth': 246, // popup width in pixels
'updateInterval': 24, // user-style automatic update interval, hours (0 = disable)
};
const values = deepCopy(defaults);
const affectsIcon = [
'show-badge',
'disableAll',
'badgeDisabled',
'badgeNormal',
];
const onChange = {
any: new Set(),
specific: new Map(),
};
// coalesce multiple pref changes in broadcast
let broadcastPrefs = {};
Object.defineProperty(this, 'readOnlyValues', {value: {}});
Object.assign(Prefs.prototype, {
get(key, defaultValue) {
if (key in values) {
return values[key];
}
if (defaultValue !== undefined) {
return defaultValue;
}
if (key in defaults) {
return defaults[key];
}
console.warn("No default preference for '%s'", key);
},
getAll() {
return deepCopy(values);
},
set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) {
const oldValue = values[key];
switch (typeof defaults[key]) {
case typeof value:
break;
case 'string':
value = String(value);
break;
case 'number':
value |= 0;
break;
case 'boolean':
value = value === true || value === 'true';
break;
}
values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value);
const hasChanged = !equal(value, oldValue);
if (!fromBroadcast) {
if (BG && BG != window) {
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync});
} else {
localStorage[key] = typeof defaults[key] == 'object'
? JSON.stringify(value)
: value;
if (broadcast && hasChanged) {
this.broadcast(key, value, {sync});
}
}
}
if (hasChanged) {
const listener = onChange.specific.get(key);
if (listener) {
listener(key, value);
}
for (const listener of onChange.any.values()) {
listener(key, value);
}
}
},
remove: key => this.set(key, undefined),
reset: key => this.set(key, deepCopy(defaults[key])),
broadcast(key, value, {sync = true} = {}) {
broadcastPrefs[key] = value;
debounce(doBroadcast);
if (sync) {
debounce(doSyncSet);
}
},
subscribe(listener, keys) {
if (keys) {
for (const key of keys) {
onChange.specific.set(key, listener);
}
} else {
onChange.any.add(listener);
}
},
});
// Unlike sync, HTML5 localStorage is ready at browser startup
// so we'll mirror the prefs to avoid using the wrong defaults
// during the startup phase
for (const key in defaults) {
const defaultValue = defaults[key];
let value = localStorage[key];
if (typeof value == 'string') {
switch (typeof defaultValue) {
case 'boolean':
value = value.toLowerCase() === 'true';
break;
case 'number':
value |= 0;
break;
case 'object':
value = tryJSONparse(value) || defaultValue;
break;
}
} else {
value = defaultValue;
}
if (BG == window) {
// when in bg page, .set() will write to localStorage
this.set(key, value, {broadcast: false, sync: false});
} else {
values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value);
}
}
if (!BG || BG == window) {
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
getSync().get('settings', ({settings: synced} = {}) => {
if (synced) {
for (const key in defaults) {
if (key == 'popupWidth' && synced[key] != values.popupWidth) {
// this is a fix for the period when popupWidth wasn't synced
// TODO: remove it in a couple of months
continue;
}
if (key in synced) {
this.set(key, synced[key], {sync: false});
}
}
}
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area == 'sync' && 'settings' in changes) {
const synced = changes.settings.newValue;
if (synced) {
for (const key in defaults) {
if (key in synced) {
this.set(key, synced[key], {sync: false});
}
}
} else {
// user manually deleted our settings, we'll recreate them
getSync().set({'settings': values});
}
}
});
}
// any access to chrome API takes time due to initialization of bindings
window.addEventListener('load', function _() {
window.removeEventListener('load', _);
chrome.runtime.onMessage.addListener(msg => {
if (msg.prefs) {
for (const id in msg.prefs) {
prefs.set(id, msg.prefs[id], {fromBroadcast: true});
}
}
});
});
return;
function doBroadcast() {
const affects = {
all: 'disableAll' in broadcastPrefs
|| 'exposeIframes' in broadcastPrefs,
};
if (!affects.all) {
for (const key in broadcastPrefs) {
affects.icon = affects.icon || affectsIcon.includes(key);
affects.popup = affects.popup || key.startsWith('popup');
affects.editor = affects.editor || key.startsWith('editor');
affects.manager = affects.manager || key.startsWith('manage');
}
}
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
broadcastPrefs = {};
}
function doSyncSet() {
getSync().set({'settings': values});
}
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
function getSync() {
if ('sync' in chrome.storage) {
return chrome.storage.sync;
}
const crappyStorage = {};
return {
get(key, callback) {
callback(crappyStorage[key] || {});
},
set(source, callback) {
for (const property in source) {
if (source.hasOwnProperty(property)) {
crappyStorage[property] = source[property];
}
}
callback();
}
};
}
function defineReadonlyProperty(obj, key, value) {
const copy = deepCopy(value);
if (typeof copy == 'object') {
Object.freeze(copy);
}
Object.defineProperty(obj, key, {value: copy, configurable: true});
}
function equal(a, b) {
if (!a || !b || typeof a != 'object' || typeof b != 'object') {
return a === b;
}
if (Object.keys(a).length != Object.keys(b).length) {
return false;
}
for (const k in a) {
if (typeof a[k] == 'object') {
if (!equal(a[k], b[k])) {
return false;
}
} else if (a[k] !== b[k]) {
return false;
}
}
return true;
}
function contextDeleteMissing() {
return (
// detect browsers without Delete by looking at the end of UA string
/Vivaldi\/[\d.]+$/.test(navigator.userAgent) ||
// Chrome and co.
/Safari\/[\d.]+$/.test(navigator.userAgent) &&
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
!Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')
);
}
}();
// Accepts an array of pref names (values are fetched via prefs.get)
// and establishes a two-way connection between the document elements and the actual prefs
function setupLivePrefs(
IDs = Object.getOwnPropertyNames(prefs.readOnlyValues)
.filter(id => document.getElementById(id))
) {
const checkedProps = {};
for (const id of IDs) {
const element = document.getElementById(id);
checkedProps[id] = element.type == 'checkbox' ? 'checked' : 'value';
updateElement({id, element, force: true});
element.addEventListener('change', onChange);
}
prefs.subscribe((id, value) => updateElement({id, value}), IDs);
function onChange() {
const value = this[checkedProps[this.id]];
if (prefs.get(this.id) != value) {
prefs.set(this.id, value);
}
}
function updateElement({
id,
value = prefs.get(id),
element = document.getElementById(id),
force,
}) {
const prop = checkedProps[id];
if (force || element[prop] != value) {
element[prop] = value;
element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
}
}
}

View File

@ -1,169 +0,0 @@
var webSqlStorage = {
migrate: function() {
if (typeof openDatabase == "undefined") {
// No WebSQL - no migration!
return;
}
webSqlStorage.getStyles(function(styles) {
getDatabase(function(db) {
var tx = db.transaction(["styles"], "readwrite");
var os = tx.objectStore("styles");
styles.forEach(function(s) {
webSqlStorage.cleanStyle(s)
os.add(s);
});
// While this was running, the styles were loaded from the (empty) indexed db
setTimeout(function() {
invalidateCache(true);
}, 500);
});
}, null);
},
cleanStyle: function(s) {
delete s.id;
s.sections.forEach(function(section) {
delete section.id;
["urls", "urlPrefixes", "domains", "regexps"].forEach(function(property) {
if (!section[property]) {
section[property] = [];
}
});
});
},
getStyles: function(callback) {
webSqlStorage.getDatabase(function(db) {
if (!db) {
callback([]);
return;
}
db.readTransaction(function (t) {
var where = "";
var params = [];
t.executeSql('SELECT DISTINCT s.*, se.id section_id, se.code, sm.name metaName, sm.value metaValue FROM styles s LEFT JOIN sections se ON se.style_id = s.id LEFT JOIN section_meta sm ON sm.section_id = se.id WHERE 1' + where + ' ORDER BY s.id, se.id, sm.id', params, function (t, r) {
var styles = [];
var currentStyle = null;
var currentSection = null;
for (var i = 0; i < r.rows.length; i++) {
var values = r.rows.item(i);
var metaName = null;
switch (values.metaName) {
case null:
break;
case "url":
metaName = "urls";
break;
case "url-prefix":
metaName = "urlPrefixes";
break;
case "domain":
var metaName = "domains";
break;
case "regexps":
var metaName = "regexps";
break;
default:
var metaName = values.metaName + "s";
}
var metaValue = values.metaValue;
if (currentStyle == null || currentStyle.id != values.id) {
currentStyle = {id: values.id, url: values.url, updateUrl: values.updateUrl, md5Url: values.md5Url, name: values.name, enabled: values.enabled == "true", originalMd5: values.originalMd5, sections: []};
styles.push(currentStyle);
}
if (values.section_id != null) {
if (currentSection == null || currentSection.id != values.section_id) {
currentSection = {id: values.section_id, code: values.code};
currentStyle.sections.push(currentSection);
}
if (metaName && metaValue) {
if (currentSection[metaName]) {
currentSection[metaName].push(metaValue);
} else {
currentSection[metaName] = [metaValue];
}
}
}
}
callback(styles);
}, reportError);
}, reportError);
}, reportError);
},
getDatabase: function(ready, error) {
try {
stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5*1024*1024);
} catch (ex) {
error();
throw ex;
}
if (stylishDb.version == "") {
// It didn't already exist, we have nothing to migrate.
ready(null);
return;
}
if (stylishDb.version == "1.0") {
webSqlStorage.dbV11(stylishDb, error, ready);
} else if (stylishDb.version == "1.1") {
webSqlStorage.dbV12(stylishDb, error, ready);
} else if (stylishDb.version == "1.2") {
webSqlStorage.dbV13(stylishDb, error, ready);
} else if (stylishDb.version == "1.3") {
webSqlStorage.dbV14(stylishDb, error, ready);
} else if (stylishDb.version == "1.4") {
webSqlStorage.dbV15(stylishDb, error, ready);
} else {
ready(stylishDb);
}
},
dbV11: function(d, error, done) {
d.changeVersion(d.version, '1.1', function (t) {
t.executeSql('CREATE TABLE styles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, code TEXT NOT NULL, enabled INTEGER NOT NULL, originalCode TEXT NULL);');
t.executeSql('CREATE TABLE style_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);');
t.executeSql('CREATE INDEX style_meta_style_id ON style_meta (style_id);');
}, error, function() { webSqlStorage.dbV12(d, error, done)});
},
dbV12: function(d, error, done) {
d.changeVersion(d.version, '1.2', function (t) {
// add section table
t.executeSql('CREATE TABLE sections (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, code TEXT NOT NULL);');
t.executeSql('INSERT INTO sections (style_id, code) SELECT id, code FROM styles;');
// switch meta to sections
t.executeSql('DROP INDEX style_meta_style_id;');
t.executeSql('CREATE TABLE section_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, section_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);');
t.executeSql('INSERT INTO section_meta (section_id, name, value) SELECT s.id, sm.name, sm.value FROM sections s INNER JOIN style_meta sm ON sm.style_id = s.style_id;');
t.executeSql('CREATE INDEX section_meta_section_id ON section_meta (section_id);');
t.executeSql('DROP TABLE style_meta;');
// drop extra fields from styles table
t.executeSql('CREATE TABLE newstyles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, enabled INTEGER NOT NULL);');
t.executeSql('INSERT INTO newstyles (id, url, updateUrl, md5Url, name, enabled) SELECT id, url, updateUrl, md5Url, name, enabled FROM styles;');
t.executeSql('DROP TABLE styles;');
t.executeSql('ALTER TABLE newstyles RENAME TO styles;');
}, error, function() { webSqlStorage.dbV13(d, error, done)});
},
dbV13: function(d, error, done) {
d.changeVersion(d.version, '1.3', function (t) {
// clear out orphans
t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);');
t.executeSql('DELETE FROM sections WHERE id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);');
}, error, function() { webSqlStorage.dbV14(d, error, done)});
},
dbV14: function(d, error, done) {
d.changeVersion(d.version, '1.4', function (t) {
t.executeSql('UPDATE styles SET url = null WHERE url = "undefined";');
}, error, function() { webSqlStorage.dbV15(d, error, done)});
},
dbV15: function(d, error, done) {
d.changeVersion(d.version, '1.5', function (t) {
t.executeSql('ALTER TABLE styles ADD COLUMN originalMd5 TEXT NULL;');
}, error, function() { done(d); });
}
}

1124
storage.js

File diff suppressed because it is too large Load Diff

249
update.js
View File

@ -1,115 +1,162 @@
/* globals getStyles, saveStyle, prefs */
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
/* global getStyleDigests, updateStyleDigest */
'use strict';
var update = {
fetch: (resource, callback) => {
let req = new XMLHttpRequest();
let [url, data] = resource.split('?');
req.open('POST', url, true);
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
req.onload = () => callback(req.responseText);
req.onerror = req.ontimeout = () => callback();
req.send(data);
},
md5Check: (style, callback, skipped) => {
let req = new XMLHttpRequest();
req.open('GET', style.md5Url, true);
req.onload = () => {
let md5 = req.responseText;
if (md5 && md5 !== style.originalMd5) {
callback(style);
}
else {
skipped(`"${style.name}" style is up-to-date`);
}
};
req.onerror = req.ontimeout = () => skipped('Error validating MD5 checksum');
req.send();
},
list: (callback) => {
getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl)));
},
perform: (observe = function () {}) => {
// from install.js
function arraysAreEqual (a, b) {
// treat empty array and undefined as equivalent
if (typeof a === 'undefined') {
return (typeof b === 'undefined') || (b.length === 0);
}
if (typeof b === 'undefined') {
return (typeof a === 'undefined') || (a.length === 0);
}
if (a.length !== b.length) {
return false;
}
return a.every(function (entry) {
return b.indexOf(entry) !== -1;
// eslint-disable-next-line no-var
var updater = {
COUNT: 'count',
UPDATED: 'updated',
SKIPPED: 'skipped',
DONE: 'done',
// details for SKIPPED status
EDITED: 'locally edited',
MAYBE_EDITED: 'may be locally edited',
SAME_MD5: 'up-to-date: MD5 is unchanged',
SAME_CODE: 'up-to-date: code sections are unchanged',
ERROR_MD5: 'error: MD5 is invalid',
ERROR_JSON: 'error: JSON is invalid',
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
updater.resetInterval();
updater.checkAllStyles.running = true;
return getStyles({}).then(styles => {
styles = styles.filter(style => style.updateUrl);
observer(updater.COUNT, styles.length);
updater.log('');
updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
return Promise.all(
styles.map(style =>
updater.checkStyle({style, observer, save, ignoreDigest})));
}).then(() => {
observer(updater.DONE);
updater.log('');
updater.checkAllStyles.running = false;
});
}
// from install.js
function sectionsAreEqual(a, b) {
if (a.code !== b.code) {
return false;
}
return ['urls', 'urlPrefixes', 'domains', 'regexps'].every(function (attribute) {
return arraysAreEqual(a[attribute], b[attribute]);
},
checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
let hasDigest;
/*
Original style digests are calculated in these cases:
* style is installed or updated from server
* style is checked for an update and its code is equal to the server code
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
* [ignoreDigest: none/false] style doesn't yet have the original digest
so we compare the code to the server code and if it's the same we save the digest,
otherwise we skip the style and report MAYBE_EDITED status
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
return getStyleDigests(style)
.then(maybeFetchMd5)
.then(maybeFetchCode)
.then(maybeSave)
.then(saved => {
observer(updater.UPDATED, saved);
updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`);
})
.catch(err => {
observer(updater.SKIPPED, style, err);
err = err === 0 ? 'server unreachable' : err;
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
});
function maybeFetchMd5([originalDigest, current]) {
hasDigest = Boolean(originalDigest);
if (hasDigest && !ignoreDigest && originalDigest != current) {
return Promise.reject(updater.EDITED);
}
return download(style.md5Url);
}
update.list(styles => {
observe('count', styles.length);
styles.forEach(style => update.md5Check(style, style => update.fetch(style.updateUrl, response => {
if (response) {
let json = JSON.parse(response);
if (json.sections.length === style.sections.length) {
if (json.sections.every((section) => {
return style.sections.some(installedSection => sectionsAreEqual(section, installedSection));
})) {
return observe('single-skipped', '2'); // everything is the same
function maybeFetchCode(md5) {
if (!md5 || md5.length != 32) {
return Promise.reject(updater.ERROR_MD5);
}
if (md5 == style.originalMd5 && hasDigest && !ignoreDigest) {
return Promise.reject(updater.SAME_MD5);
}
return download(style.updateUrl);
}
function maybeSave(text) {
const json = tryJSONparse(text);
if (!styleJSONseemsValid(json)) {
return Promise.reject(updater.ERROR_JSON);
}
json.method = 'saveStyle';
json.id = style.id;
if (styleSectionsEqual(json, style)) {
// JSONs may have different order of items even if sections are effectively equal
// so we'll update the digest anyway
updateStyleDigest(json);
return Promise.reject(updater.SAME_CODE);
} else if (!hasDigest && !ignoreDigest) {
return Promise.reject(updater.MAYBE_EDITED);
}
return !save ? json :
saveStyle(Object.assign(json, {
name: null, // keep local name customizations
reason: 'update',
}));
}
saveStyle(json).then(style => {
observe('single-updated', style.name);
});
function styleJSONseemsValid(json) {
return json
&& json.sections
&& json.sections.length
&& typeof json.sections.every == 'function'
&& typeof json.sections[0].code == 'string';
}
else {
return observe('single-skipped', '3'); // style sections mismatch
}
}
}), () => observe('single-skipped', '1')));
});
}
};
// automatically update all user-styles if "updateInterval" pref is set
window.setTimeout(function () {
let id;
function run () {
update.perform(/*(cmd, value) => console.log(cmd, value)*/);
reset();
}
function reset () {
window.clearTimeout(id);
let interval = prefs.get('updateInterval');
// if interval === 0 => automatic update is disabled
},
schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval) {
/* console.log('next update', interval); */
id = window.setTimeout(run, interval * 60 * 60 * 1000);
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
} else {
debounce.unregister(updater.checkAllStyles);
}
},
resetInterval() {
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
updater.schedule();
},
log: (() => {
let queue = [];
let lastWriteTime = 0;
return text => {
queue.push({text, time: new Date().toLocaleString()});
debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0);
};
function flushQueue() {
chromeLocal.getValue('updateLog').then((lines = []) => {
const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
if (!queue[0].text) {
queue.shift();
if (lines[lines.length - 1]) {
lines.push('');
}
}
if (prefs.get('updateInterval')) {
run();
}
chrome.runtime.onMessage.addListener(request => {
// when user has changed the predefined time interval in the settings page
if (request.method === 'prefChanged' && request.prefName === 'updateInterval') {
reset();
}
// when user just manually checked for updates
if (request.method === 'resetInterval') {
reset();
}
lines.splice(0, lines.length - 1000);
lines.push(time + queue[0].text);
lines.push(...queue.slice(1).map(item => item.text));
chromeLocal.setValue('updateLog', lines);
lastWriteTime = Date.now();
queue = [];
});
}, 10000);
}
})(),
};
updater.schedule();
prefs.subscribe(updater.schedule, ['updateInterval']);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB