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_style = space
indent_size = 2 indent_size = 2
tab_width = 2 tab_width = 2
end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true

108
.eslintrc
View File

@ -1,43 +1,59 @@
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md # https://github.com/eslint/eslint/blob/master/docs/rules/README.md
parserOptions: parserOptions:
ecmaVersion: 2017 ecmaVersion: 2015
env: env:
browser: true browser: true
commonjs: true
es6: true es6: true
webextensions: true webextensions: true
globals: globals:
CodeMirror: false # messaging.js
runTryCatch: true KEEP_CHANNEL_OPEN: false
getStyles: true FIREFOX: false
updateIcon: true OPERA: false
saveStyle: true URLS: false
invalidateCache: true BG: false
getDatabase: true 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 prefs: false
reportError: true setupLivePrefs: false
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
rules: rules:
accessor-pairs: [2] accessor-pairs: [2]
@ -47,7 +63,7 @@ rules:
arrow-parens: [2, as-needed] arrow-parens: [2, as-needed]
arrow-spacing: [2, {before: true, after: true}] arrow-spacing: [2, {before: true, after: true}]
block-scoped-var: [2] block-scoped-var: [2]
brace-style: [2, 1tbs, {allowSingleLine: true}] brace-style: [2, 1tbs, {allowSingleLine: false}]
camelcase: [2, {properties: never}] camelcase: [2, {properties: never}]
class-methods-use-this: [2] class-methods-use-this: [2]
comma-dangle: [0] comma-dangle: [0]
@ -68,16 +84,15 @@ rules:
func-names: [0] func-names: [0]
generator-star-spacing: [2, before] generator-star-spacing: [2, before]
global-require: [0] global-require: [0]
guard-for-in: [2] guard-for-in: [0] # not needed for our non-OOP stuff
handle-callback-err: [2, ^(err|error)$] handle-callback-err: [2, ^(err|error)$]
id-blacklist: [0] id-blacklist: [0]
id-length: [0] id-length: [0]
id-match: [0] id-match: [0]
indent: [2, 2, {VariableDeclarator: 0}] indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
jsx-quotes: [0] jsx-quotes: [0]
key-spacing: [0] key-spacing: [0]
keyword-spacing: [2] keyword-spacing: [2]
linebreak-style: [2, unix]
lines-around-comment: [0] lines-around-comment: [0]
lines-around-directive: [0] lines-around-directive: [0]
max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}] max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}]
@ -98,7 +113,7 @@ rules:
no-case-declarations: [2] no-case-declarations: [2]
no-class-assign: [2] no-class-assign: [2]
no-cond-assign: [2, except-parens] no-cond-assign: [2, except-parens]
no-confusing-arrow: [2] no-confusing-arrow: [1, {allowParens: true}]
no-const-assign: [2] no-const-assign: [2]
no-constant-condition: [0] no-constant-condition: [0]
no-continue: [0] no-continue: [0]
@ -125,11 +140,11 @@ rules:
no-extra-label: [0] no-extra-label: [0]
no-extra-parens: [0] no-extra-parens: [0]
no-extra-semi: [2] no-extra-semi: [2]
no-fallthrough: [2] no-fallthrough: [2, {commentPattern: fallthrough.*}]
no-floating-decimal: [0] no-floating-decimal: [0]
no-func-assign: [2] no-func-assign: [2]
no-global-assign: [2] no-global-assign: [2]
no-implicit-coercion: [2] no-implicit-coercion: [1]
no-implicit-globals: [0] no-implicit-globals: [0]
no-implied-eval: [2] no-implied-eval: [2]
no-inline-comments: [0] no-inline-comments: [0]
@ -139,7 +154,7 @@ rules:
no-irregular-whitespace: [2] no-irregular-whitespace: [2]
no-iterator: [2] no-iterator: [2]
no-label-var: [2] no-label-var: [2]
no-labels: [2] no-labels: [2, {allowLoop: true}]
no-lone-blocks: [2] no-lone-blocks: [2]
no-lonely-if: [0] no-lonely-if: [0]
no-loop-func: [0] no-loop-func: [0]
@ -149,7 +164,7 @@ rules:
no-mixed-spaces-and-tabs: [2] no-mixed-spaces-and-tabs: [2]
no-multi-spaces: [0] no-multi-spaces: [0]
no-multi-str: [2] 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-native-reassign: [2]
no-negated-condition: [0] no-negated-condition: [0]
no-negated-in-lhs: [2] no-negated-in-lhs: [2]
@ -184,43 +199,44 @@ rules:
no-tabs: [2] no-tabs: [2]
no-template-curly-in-string: [2] no-template-curly-in-string: [2]
no-this-before-super: [2] no-this-before-super: [2]
no-throw-literal: [2] no-throw-literal: [0]
no-trailing-spaces: [2] no-trailing-spaces: [2]
no-undef-init: [2] no-undef-init: [2]
no-undef: [2] no-undef: [2]
no-undefined: [0] no-undefined: [0]
no-underscore-dangle: [0] no-underscore-dangle: [0]
no-unexpected-multiline: [2] no-unexpected-multiline: [2]
no-unmodified-loop-condition: [2] no-unmodified-loop-condition: [1]
no-unneeded-ternary: [2] no-unneeded-ternary: [2]
no-unreachable: [2] no-unreachable: [2]
no-unsafe-finally: [2] no-unsafe-finally: [2]
no-unsafe-negation: [2] no-unsafe-negation: [2]
no-unused-expressions: [2] no-unused-expressions: [2]
no-unused-labels: [0] 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-use-before-define: [2, nofunc]
no-useless-call: [2] no-useless-call: [2]
no-useless-computed-key: [2] no-useless-computed-key: [2]
no-useless-concat: [2] no-useless-concat: [2]
no-useless-constructor: [2] no-useless-constructor: [2]
no-useless-escape: [2] no-useless-escape: [2]
no-var: [0] no-var: [1]
no-warning-comments: [0] no-warning-comments: [0]
no-whitespace-before-property: [2] no-whitespace-before-property: [2]
no-with: [2] no-with: [2]
object-curly-newline: [0] object-curly-newline: [0]
object-curly-spacing: [2, never] object-curly-spacing: [2, never]
object-shorthand: [0] object-shorthand: [0]
one-var-declaration-per-line: [0] one-var-declaration-per-line: [1]
one-var: [0] one-var: [0]
operator-assignment: [2, always] operator-assignment: [2, always]
operator-linebreak: [2, after] operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
padded-blocks: [2, never] padded-blocks: [2, never]
prefer-numeric-literals: [2] prefer-numeric-literals: [2]
prefer-rest-params: [0] prefer-rest-params: [0]
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}]
quote-props: [0] quote-props: [0]
quotes: [2, double, avoid-escape] quotes: [1, single, avoid-escape]
radix: [2, as-needed] radix: [2, as-needed]
require-jsdoc: [0] require-jsdoc: [0]
require-yield: [2] require-yield: [2]
@ -233,7 +249,7 @@ rules:
space-in-parens: [2, never] space-in-parens: [2, never]
space-infix-ops: [2] space-infix-ops: [2]
space-unary-ops: [2] space-unary-ops: [2]
spaced-comment: [2, always, {markers: ["!"]}] spaced-comment: [0, always, {markers: ["!"]}]
strict: [2, global] strict: [2, global]
symbol-description: [2] symbol-description: [2]
template-curly-spacing: [2, never] template-curly-spacing: [2, never]

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "هل تريد بالتأكيد حذف هذا النمط؟", "message": "هل تريد بالتأكيد حذف هذا النمط؟",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "إزالة القسم", "message": "إزالة القسم",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "výchozí", "message": "výchozí",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Exportovat", "message": "Exportovat",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Opravdu chcete tento styl smazat?", "message": "Opravdu chcete tento styl smazat?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla Formát", "message": "Mozilla Formát",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus nefunguje na těchto stránkách.)", "message": "Stylus nefunguje na těchto stránkách.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -11,10 +11,6 @@
"message": "Styles Exportieren", "message": "Styles Exportieren",
"description": "" "description": ""
}, },
"manageOnlyEdited": {
"message": "Nur bearbeitete Styles",
"description": "Checkbox to show only locally edited styles"
},
"optionsUpdateInterval": { "optionsUpdateInterval": {
"message": "Automatischer Update- und Installations-Intervall (in Stunden)", "message": "Automatischer Update- und Installations-Intervall (in Stunden)",
"description": "" "description": ""
@ -83,7 +79,7 @@
"message": "Hilfe", "message": "Hilfe",
"description": "Alternate text for help buttons" "description": "Alternate text for help buttons"
}, },
"confirmOK": { "confirmDelete": {
"message": "Löschen", "message": "Löschen",
"description": "" "description": ""
}, },
@ -169,7 +165,7 @@
"description": "" "description": ""
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -356,8 +352,8 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus funktioniert nicht auf Seiten wie diesen.)", "message": "Stylus funktioniert nicht auf Seiten wie diesen.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {
@ -520,4 +516,4 @@
"message": "Gestalten Sie das Web mit Stylus, einem Manager für Benutzer-Styles, um. Stylus lässt Sie ganz einfach Themes und Skins für viele beliebte Webseiten installieren.", "message": "Gestalten Sie das Web mit Stylus, einem Manager für Benutzer-Styles, um. Stylus lässt Sie ganz einfach Themes und Skins für viele beliebte Webseiten installieren.",
"description": "Extension description" "description": "Extension description"
} }
} }

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"manageOnlyEdited": {
"message": "Μόνο επεξεργασμενα στυλ",
"description": "Checkbox to show only locally edited styles"
},
"exportLabel": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;", "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.)", "message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -34,7 +34,7 @@
}, },
"appliesLabel": { "appliesLabel": {
"message": "Applies to", "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": { "appliesRegexpOption": {
"message": "URLs matching the regexp", "message": "URLs matching the regexp",
@ -76,6 +76,13 @@
"message": "Check all styles for updates", "message": "Check all styles for updates",
"description": "Label for the button to 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": { "checkForUpdate": {
"message": "Check for update", "message": "Check for update",
"description": "Label for the button to check a single style for an update" "description": "Label for the button to check a single style for an update"
@ -108,6 +115,26 @@
"message": "Theme", "message": "Theme",
"description": "Label for the style editor's CSS 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": { "confirmNo": {
"message": "No", "message": "No",
"description": "'No' button in a confirm dialog" "description": "'No' button in a confirm dialog"
@ -141,6 +168,9 @@
"description": "Drag'n'drop message" "description": "Drag'n'drop message"
}, },
"confirmOK": { "confirmOK": {
"message": "OK"
},
"confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -191,6 +221,10 @@
"message": "Enable", "message": "Enable",
"description": "Label for the button to enable a style" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -211,6 +245,46 @@
"message": "Type a command name", "message": "Type a command name",
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short" "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": { "importLabel": {
"message": "Import", "message": "Import",
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
@ -240,7 +314,7 @@
"description": "Label for the CSSLint issues block on the style edit page" "description": "Label for the CSSLint issues block on the style edit page"
}, },
"issuesHelp": { "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" "description": "Help popup message for the CSSLint issues block on the style edit page"
}, },
"manageFilters": { "manageFilters": {
@ -255,12 +329,40 @@
"message": "Only enabled styles", "message": "Only enabled styles",
"description": "Checkbox to show only enabled styles" "description": "Checkbox to show only enabled styles"
}, },
"manageOnlyEdited": { "manageOnlyLocal": {
"message": "Only edited styles", "message": "Only locally created styles",
"description": "Checkbox to show only locally edited 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": { "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" "description": "Help text on the manage page"
}, },
"manageTitle": { "manageTitle": {
@ -287,14 +389,6 @@
"message": "Options", "message": "Options",
"description": "Go to Options UI" "description": "Go to Options UI"
}, },
"openOptionsShortcuts": {
"message": "Shortcuts",
"description": "Go to shortcut configuration"
},
"openShortcutsPopup": {
"message": "Shortcuts",
"description": "Go to shortcut configuration"
},
"optionsHeading": { "optionsHeading": {
"message": "Options", "message": "Options",
"description": "Heading for options section on manage page." "description": "Heading for options section on manage page."
@ -304,11 +398,11 @@
"description": "Subheading for options section on manage page." "description": "Subheading for options section on manage page."
}, },
"popupStylesFirst": { "popupStylesFirst": {
"message": "List styles before commands in the toolbar button menu", "message": "Styles before commands",
"description": "Label for the checkbox controlling section order in the toolbar button menu." "description": "Label for the checkbox controlling section order in the popup."
}, },
"prefShowBadge": { "prefShowBadge": {
"message": "Show number of styles active for the current site on the toolbar button", "message": "Number of styles active for the current site",
"description": "Label for the checkbox controlling toolbar badge text." "description": "Label for the checkbox controlling toolbar badge text."
}, },
"replace": { "replace": {
@ -351,10 +445,55 @@
"message": "Remove section", "message": "Remove section",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"
}, },
"shortcuts": {
"message": "Shortcuts",
"description": "Go to shortcut configuration"
},
"shortcutsNote": {
"message": "Define keyboard shortcuts"
},
"styleBadRegexp": { "styleBadRegexp": {
"message": "Regexp is invalid.", "message": "Regexp is invalid.",
"description": "Validation message for a bad regexp in a style" "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": { "styleBeautify": {
"message": "Beautify", "message": "Beautify",
"description": "Label for the CSS-beautifier button on the edit style page" "description": "Label for the CSS-beautifier button on the edit style page"
@ -417,10 +556,18 @@
} }
} }
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus can't affect this page.)", "message": "Stylus doesn't work on pages like this.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "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": { "undo": {
"message": "Undo", "message": "Undo",
"description": "Button label" "description": "Button label"
@ -429,6 +576,14 @@
"message": "Undo (global)", "message": "Undo (global)",
"description": "CSS-beautify global Undo button label" "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": { "updateCheckFailBadResponseCode": {
"message": "Update failed - server responded with code $code$.", "message": "Update failed - server responded with code $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error", "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.", "message": "Update failed - server unreachable.",
"description": "Text that displays when an update check failed because the update server is 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": { "updateCheckSucceededNoUpdate": {
"message": "Style is up to date.", "message": "Style is up to date.",
"description": "Text that displays when an update check completed and no update is available" "description": "Text that displays when an update check completed and no update is available"
}, },
"updateAllCheckSucceededNoUpdate": { "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" "description": "Text that displays when an update all check completed and no updates are available"
}, },
"updateCompleted": { "updateCompleted": {
"message": "Update completed.", "message": "Update completed.",
"description": "Text that displays when an 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": { "writeStyleFor": {
"message": "Write style for: ", "message": "Write style for: ",
"description": "Label for toolbar pop-up that precedes the links to write a new style" "description": "Label for toolbar pop-up that precedes the links to write a new style"
@ -469,10 +648,10 @@
"message": "Import styles" "message": "Import styles"
}, },
"optionsBadgeNormal": { "optionsBadgeNormal": {
"message": "Badge background color" "message": "Background color"
}, },
"optionsBadgeDisabled": { "optionsBadgeDisabled": {
"message": "Badge background color (when disabled)" "message": "Background color when disabled"
}, },
"optionsPopupWidth": { "optionsPopupWidth": {
"message": "Popup width (in pixels)" "message": "Popup width (in pixels)"
@ -481,19 +660,43 @@
"message": "Automatically check for and install all available userstyle updates (in hrs)" "message": "Automatically check for and install all available userstyle updates (in hrs)"
}, },
"optionsUpdateIntervalNote": { "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": { "optionsUpdateImportNote": {
"message": "UI Customizations" "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": { "optionsActions": {
"message": "Actions" "message": "Actions"
}, },
"optionsOpenManager": { "optionsReset": {
"message": "Open styles manager" "message": "Reset the options to default values"
}, },
"optionsOpenManagerNote": { "optionsResetButton": {
"message": "Define a keyboard shortcut" "message": "Reset options"
},
"optionsOpenManager": {
"message": "Manage styles"
}, },
"optionsCheckUpdate": { "optionsCheckUpdate": {
"message": "Check for and install all available updates" "message": "Check for and install all available updates"
@ -502,6 +705,6 @@
"message": "Open" "message": "Open"
}, },
"optionsCheck": { "optionsCheck": {
"message": "Check" "message": "Update styles"
} }
} }

View File

@ -11,10 +11,6 @@
"message": "Exportar estilos", "message": "Exportar estilos",
"description": "" "description": ""
}, },
"manageOnlyEdited": {
"message": "Sólo estilos editados",
"description": "Checkbox to show only locally edited styles"
},
"optionsUpdateInterval": { "optionsUpdateInterval": {
"message": "Buscar e instalar automáticamente todas las actualizaciones disponibles de estilos de usuario (en horas)", "message": "Buscar e instalar automáticamente todas las actualizaciones disponibles de estilos de usuario (en horas)",
"description": "" "description": ""
@ -157,7 +153,7 @@
"description": "" "description": ""
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -212,7 +208,7 @@
"message": "¿Está seguro de que quiere eliminar este estilo?", "message": "¿Está seguro de que quiere eliminar este estilo?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -350,8 +346,8 @@
"message": "Formato Mozilla", "message": "Formato Mozilla",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus no funciona en páginas como esta)", "message": "Stylus no funciona en páginas como esta",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Oletko varma että haluat poistaa tämän tyylin?", "message": "Oletko varma että haluat poistaa tämän tyylin?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "Poista osio", "message": "Poista osio",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "défaut", "message": "défaut",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Exportez", "message": "Exportez",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Voulez-vous vraiment supprimer ce style ?", "message": "Voulez-vous vraiment supprimer ce style ?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus ne fonctionne pas sur les pages de ce genre)", "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" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Vuoi eliminare questo stile?", "message": "Vuoi eliminare questo stile?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "Rimuovi sezione", "message": "Rimuovi sezione",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "このスタイルを削除してもよろしいですか?", "message": "このスタイルを削除してもよろしいですか?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "セクションを削除", "message": "セクションを削除",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "standaard", "message": "standaard",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Exporteren", "message": "Exporteren",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Weet u zeker dat u deze stijl wilt verwijderen?", "message": "Weet u zeker dat u deze stijl wilt verwijderen?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla-opmaak", "message": "Mozilla-opmaak",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus werkt niet op pagina's als deze.)", "message": "Stylus werkt niet op pagina's als deze.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Tem certeza de que deseja excluir este estilo?", "message": "Tem certeza de que deseja excluir este estilo?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "Remover seção", "message": "Remover seção",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "по-умолчанию", "message": "по-умолчанию",
"description": "Default CodeMirror CSS theme option on the edit style page" "description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"manageOnlyEdited": {
"message": "Только отредактированные стили",
"description": "Checkbox to show only locally edited styles"
},
"exportLabel": { "exportLabel": {
"message": "Экспорт", "message": "Экспорт",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Удалить этот стиль?", "message": "Удалить этот стиль?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Формат Mozilla", "message": "Формат Mozilla",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus не работает на таких страницах)", "message": "Stylus не работает на таких страницах.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

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

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Är du säker på att du vill ta bort denna stil?", "message": "Är du säker på att du vill ta bort denna stil?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus fungerar inte på sidor som denna.)", "message": "Stylus fungerar inte på sidor som denna.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Är du säker på att du vill ta bort denna stil?", "message": "Är du säker på att du vill ta bort denna stil?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus fungerar inte på sidor som dessa.)", "message": "Stylus fungerar inte på sidor som dessa.",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?", "message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "Remove section", "message": "Remove section",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "Bu stili silmek istediğinizden emin misiniz?", "message": "Bu stili silmek istediğinizden emin misiniz?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "Bölümü kaldır", "message": "Bölümü kaldır",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -7,10 +7,6 @@
"message": "default", "message": "default",
"description": "Default CodeMirror CSS theme option on the edit style page" "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": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "确定要删除这个样式吗?", "message": "确定要删除这个样式吗?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,10 +290,6 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"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": { "sectionRemove": {
"message": "移除节", "message": "移除节",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"

View File

@ -11,10 +11,6 @@
"message": "导出所有样式", "message": "导出所有样式",
"description": "" "description": ""
}, },
"manageOnlyEdited": {
"message": "仅修改过的样式",
"description": "Checkbox to show only locally edited styles"
},
"optionsUpdateInterval": { "optionsUpdateInterval": {
"message": "每 N 小时检查所有样式更新0 为关闭检查)", "message": "每 N 小时检查所有样式更新0 为关闭检查)",
"description": "" "description": ""
@ -79,7 +75,7 @@
"message": "帮助", "message": "帮助",
"description": "Alternate text for help buttons" "description": "Alternate text for help buttons"
}, },
"confirmOK": { "confirmDelete": {
"message": "确定", "message": "确定",
"description": "" "description": ""
}, },
@ -165,7 +161,7 @@
"description": "" "description": ""
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -352,8 +348,8 @@
"message": "Mozilla 格式", "message": "Mozilla 格式",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": "(Stylus在这样的页面上不工作)", "message": "Stylus在这样的页面上不工作",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

View File

@ -7,10 +7,6 @@
"message": "默認", "message": "默認",
"description": "Default CodeMirror CSS theme option on the edit style page" "description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"manageOnlyEdited": {
"message": "只顯示已禁用的樣式",
"description": "Checkbox to show only locally edited styles"
},
"exportLabel": { "exportLabel": {
"message": "導出", "message": "導出",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -117,7 +113,7 @@
} }
}, },
"manageText": { "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" "description": "Help text on the manage page"
}, },
"searchStyles": { "searchStyles": {
@ -168,7 +164,7 @@
"message": "確定要刪除這個樣式嗎?", "message": "確定要刪除這個樣式嗎?",
"description": "Confirmation before deleting a style" "description": "Confirmation before deleting a style"
}, },
"confirmOK": { "confirmDelete": {
"message": "Delete" "message": "Delete"
}, },
"confirmCancel": { "confirmCancel": {
@ -294,8 +290,8 @@
"message": "Mozilla格式", "message": "Mozilla格式",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"stylishUnavailableForURL": { "stylusUnavailableForURL": {
"message": " Stylus 不能在諸如此類的網頁上生效。", "message": "Stylus 不能在諸如此類的網頁上生效。",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"sectionRemove": { "sectionRemove": {

628
apply.js
View File

@ -1,352 +1,336 @@
// using ES5 syntax because ES6 is fast only since around Chrome 55 // Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/
// so we'll wait until Chrome 60 arguably before converting // like destructring, classes, defaults, spread, calculated key names
/* eslint no-var: 0 */
'use strict';
var g_disableAll = false; var ID_PREFIX = 'stylus-';
var g_styleElements = {}; var ROOT = document.documentElement;
var iframeObserver; var isOwnPage = location.href.startsWith('chrome-extension:');
var retiredStyleIds = []; var disableAll = false;
var exposeIframes = false;
var styleElements = new Map();
var disabledElements = new Map();
var retiredStyleTimers = new Map();
var docRewriteObserver;
initObserver();
requestStyles(); 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); chrome.runtime.onMessage.addListener(applyOnMessage);
if (!isOwnPage) {
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
window.addEventListener(chrome.runtime.id, orphanCheck, true);
}
function requestStyles(options, callback = applyStyles) {
var matchUrl = location.href;
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
// dynamic about: and javascript: iframes don't have an URL yet
// so we'll try the parent frame which is guaranteed to have a real URL
try {
if (window != parent) {
matchUrl = parent.location.href;
}
} catch (e) {}
}
const request = Object.assign({
method: 'getStyles',
matchUrl,
enabled: true,
asHash: true,
}, options);
// On own pages we request the styles directly to minimize delay and flicker
if (typeof getStylesSafe !== 'undefined') {
getStylesSafe(request).then(callback);
} else {
chrome.runtime.sendMessage(request, callback);
}
}
function applyOnMessage(request, sender, sendResponse) { function applyOnMessage(request, sender, sendResponse) {
// Also handle special request just for the pop-up if (request.styles == 'DIY') {
switch (request.method == "updatePopup" ? request.reason : request.method) { // Do-It-Yourself tells our built-in pages to fetch the styles directly
case "styleDeleted": // which is faster because IPC messaging JSON-ifies everything internally
removeStyle(request.id, document); requestStyles({}, styles => {
break; request.styles = styles;
case "styleUpdated": applyOnMessage(request);
if (request.style.enabled) { });
retireStyle(request.style.id); return;
// fallthrough to "styleAdded" }
} else {
removeStyle(request.style.id, document); switch (request.method) {
break;
} case 'styleDeleted':
case "styleAdded": removeStyle(request);
if (request.style.enabled) { break;
chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles);
} case 'styleUpdated':
break; if (request.codeIsUpdated === false) {
case "styleApply": applyStyleState(request.style);
applyStyles(request.styles); break;
break; }
case "styleReplaceAll": if (request.style.enabled) {
replaceAll(request.styles, document); removeStyle({id: request.style.id, retire: true});
break; requestStyles({id: request.style.id});
case "styleDisableAll": } else {
disableAll(request.disableAll); removeStyle(request.style);
break; }
case "ping": break;
sendResponse(true);
break; case 'styleAdded':
} if (request.style.enabled) {
requestStyles({id: request.style.id});
}
break;
case 'styleApply':
applyStyles(request.styles);
break;
case 'styleReplaceAll':
replaceAll(request.styles);
break;
case 'prefChanged':
if ('disableAll' in request.prefs) {
doDisableAll(request.prefs.disableAll);
}
if ('exposeIframes' in request.prefs) {
doExposeIframes(request.prefs.exposeIframes);
}
break;
case 'ping':
sendResponse(true);
break;
}
} }
function disableAll(disable) {
if (!disable === !g_disableAll) {
return;
}
g_disableAll = disable;
if (g_disableAll) {
iframeObserver.disconnect();
}
disableSheets(g_disableAll, document); function doDisableAll(disable = disableAll) {
if (!disable === !disableAll) {
if (!g_disableAll && document.readyState != "loading") { return;
iframeObserver.start(); }
} disableAll = disable;
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
function disableSheets(disable, doc) { if (stylesheet.ownerNode.matches(`STYLE.stylus[id^="${ID_PREFIX}"]`)
Array.prototype.forEach.call(doc.styleSheets, function(stylesheet) { && stylesheet.disabled != disable) {
if (stylesheet.ownerNode.classList.contains("stylus")) { 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 removeStyle(id, doc) {
var e = doc.getElementById("stylus-" + id); function doExposeIframes(state = exposeIframes) {
delete g_styleElements["stylus-" + id]; if (state === exposeIframes || window == parent) {
if (e) { return;
e.remove(); }
} exposeIframes = state;
if (doc == document && Object.keys(g_styleElements).length == 0) { const attr = document.documentElement.getAttribute('stylus-iframe');
iframeObserver.disconnect(); if (state && attr != '') {
} document.documentElement.setAttribute('stylus-iframe', '');
getDynamicIFrames(doc).forEach(function(iframe) { } else if (!state && attr == '') {
removeStyle(id, iframe.contentDocument); document.documentElement.removeAttribute('stylus-iframe');
}); }
} }
// to avoid page flicker when the style is updated
// instead of removing it immediately we rename its ID and queue it function applyStyleState({id, enabled}) {
// to be deleted in applyStyles after a new version is fetched and applied const inCache = disabledElements.get(id) || styleElements.get(id);
function retireStyle(id, doc) { const inDoc = document.getElementById(ID_PREFIX + id);
var deadID = "ghost-" + id; if (enabled) {
if (!doc) { if (inDoc) {
doc = document; return;
retiredStyleIds.push(deadID); } else if (inCache) {
delete g_styleElements["stylus-" + id]; addStyleElement(inCache);
// in case something went wrong and new style was never applied disabledElements.delete(id);
setTimeout(removeStyle.bind(null, deadID, doc), 1000); } else {
} requestStyles({id});
var e = doc.getElementById("stylus-" + id); }
if (e) { } else {
e.id = "stylus-" + deadID; if (inDoc) {
} disabledElements.set(id, inDoc);
getDynamicIFrames(doc).forEach(function(iframe) { inDoc.remove();
retireStyle(id, iframe.contentDocument); }
}); }
} }
function applyStyles(styleHash) {
if (!styleHash) { // Chrome is starting up
requestStyles();
return;
}
if ("disableAll" in styleHash) {
disableAll(styleHash.disableAll);
delete styleHash.disableAll;
}
for (var styleId in styleHash) { function removeStyle({id, retire = false}) {
applySections(styleId, styleHash[styleId]); const el = document.getElementById(ID_PREFIX + id);
} if (el) {
if (retire) {
if (Object.keys(g_styleElements).length) { // to avoid page flicker when the style is updated
// when site response is application/xml Chrome displays our style elements // instead of removing it immediately we rename its ID and queue it
// under document.documentElement as plain text so we need to move them into HEAD // to be deleted in applyStyles after a new version is fetched and applied
// (which already is autogenerated at this moment for the xml response) const deadID = 'ghost-' + id;
if (document.head && document.head.firstChild && document.head.firstChild.id == "xml-viewer-style") { el.id = ID_PREFIX + deadID;
for (var id in g_styleElements) { // in case something went wrong and new style was never applied
document.head.appendChild(document.getElementById(id)); retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
} } else {
} el.remove();
document.addEventListener("DOMContentLoaded", onDOMContentLoaded); }
} }
styleElements.delete(ID_PREFIX + id);
if (retiredStyleIds.length) { disabledElements.delete(id);
setTimeout(function() { retiredStyleTimers.delete(id);
while (retiredStyleIds.length) {
removeStyle(retiredStyleIds.shift(), document);
}
}, 0);
}
} }
function onDOMContentLoaded() {
addDocumentStylesToAllIFrames(); function applyStyles(styles) {
iframeObserver.start(); if (!styles) {
// Chrome is starting up
requestStyles();
return;
}
if ('disableAll' in styles) {
doDisableAll(styles.disableAll);
delete styles.disableAll;
}
if ('exposeIframes' in styles) {
doExposeIframes(styles.exposeIframes);
delete styles.exposeIframes;
}
if (document.head
&& document.head.firstChild
&& document.head.firstChild.id == 'xml-viewer-style') {
// when site response is application/xml Chrome displays our style elements
// under document.documentElement as plain text so we need to move them into HEAD
// which is already autogenerated at this moment
ROOT = document.head;
}
for (const id in styles) {
applySections(id, styles[id]);
}
initDocRewriteObserver();
if (retiredStyleTimers.size) {
setTimeout(() => {
for (const [id, timer] of retiredStyleTimers.entries()) {
removeStyle({id});
clearTimeout(timer);
}
});
}
} }
function applySections(styleId, sections) { function applySections(styleId, sections) {
var styleElement = document.getElementById("stylus-" + styleId); let el = document.getElementById(ID_PREFIX + styleId);
// Already there. if (el) {
if (styleElement) { return;
return; }
} if (document.documentElement instanceof SVGSVGElement) {
if (document.documentElement instanceof SVGSVGElement) { // SVG document style
// SVG document, make an SVG style element. el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style"); } else if (document instanceof XMLDocument) {
} else { // XML document style
// This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too. el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
styleElement = document.createElement("style"); } else {
} // HTML document style; also works on HTML-embedded SVG
styleElement.setAttribute("id", "stylus-" + styleId); el = document.createElement('style');
styleElement.setAttribute("class", "stylus"); }
styleElement.setAttribute("type", "text/css"); Object.assign(el, {
styleElement.appendChild(document.createTextNode(sections.map(function(section) { id: ID_PREFIX + styleId,
return section.code; className: 'stylus',
}).join("\n"))); type: 'text/css',
addStyleElement(styleElement, document); textContent: sections.map(section => section.code).join('\n'),
g_styleElements[styleElement.id] = styleElement; });
addStyleElement(el);
styleElements.set(el.id, el);
disabledElements.delete(styleId);
} }
function addStyleElement(styleElement, doc) {
if (!doc.documentElement || doc.getElementById(styleElement.id)) { function addStyleElement(el) {
return; if (ROOT && !document.getElementById(el.id)) {
} ROOT.appendChild(el);
doc.documentElement.appendChild(doc.importNode(styleElement, true)) el.disabled = disableAll;
.disabled = g_disableAll; }
getDynamicIFrames(doc).forEach(function(iframe) {
if (iframeIsLoadingSrcDoc(iframe)) {
addStyleToIFrameSrcDoc(iframe, styleElement);
} else {
addStyleElement(styleElement, iframe.contentDocument);
}
});
} }
function addDocumentStylesToIFrame(iframe) {
var doc = iframe.contentDocument; function replaceAll(newStyles) {
var srcDocIsLoading = iframeIsLoadingSrcDoc(iframe); const oldStyles = Array.prototype.slice.call(
for (var id in g_styleElements) { document.querySelectorAll(`STYLE.stylus[id^="${ID_PREFIX}"]`));
if (srcDocIsLoading) { oldStyles.forEach(el => (el.id += '-ghost'));
addStyleToIFrameSrcDoc(iframe, g_styleElements[id]); styleElements.clear();
} else { disabledElements.clear();
addStyleElement(g_styleElements[id], doc); [...retiredStyleTimers.values()].forEach(clearTimeout);
} retiredStyleTimers.clear();
} applyStyles(newStyles);
oldStyles.forEach(el => el.remove());
} }
function addDocumentStylesToAllIFrames() {
getDynamicIFrames(document).forEach(addDocumentStylesToIFrame); function initDocRewriteObserver() {
if (isOwnPage || docRewriteObserver || !styleElements.size) {
return;
}
// re-add styles if we detect documentElement being recreated
const reinjectStyles = () => {
if (!styleElements) {
return orphanCheck && orphanCheck();
}
ROOT = document.documentElement;
for (const el of styleElements.values()) {
addStyleElement(document.importNode(el, true));
}
};
// detect documentElement being rewritten from inside the script
docRewriteObserver = new MutationObserver(mutations => {
for (let m = mutations.length; --m >= 0;) {
const added = mutations[m].addedNodes;
for (let n = added.length; --n >= 0;) {
if (added[n].localName == 'html') {
reinjectStyles();
return;
}
}
}
});
docRewriteObserver.observe(document, {childList: true});
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
setTimeout(() => {
if (document.documentElement != ROOT) {
reinjectStyles();
}
});
} }
// Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs.
function getDynamicIFrames(doc) { function orphanCheck() {
return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); const port = chrome.runtime.connect();
} if (port) {
port.disconnect();
function iframeIsDynamic(f) { return;
var href; }
try {
href = f.contentDocument.location.href; // we're orphaned due to an extension update
} catch (ex) { // we can detach the mutation observer
// Cross-origin, so it's not a dynamic iframe if (docRewriteObserver) {
return false; docRewriteObserver.disconnect();
} }
return href == document.location.href || href.indexOf("about:") == 0; // we can detach event listeners
} window.removeEventListener(chrome.runtime.id, orphanCheck, true);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
function iframeIsLoadingSrcDoc(f) { // we can destroy our globals in this context to free up memory
return f.srcdoc && f.contentDocument.all.length <= 3; [ // functions
// 3 nodes or less in total (html, head, body) == new empty iframe about to be overwritten by its 'srcdoc' 'addStyleElement',
} 'applyOnMessage',
'applySections',
function addStyleToIFrameSrcDoc(iframe, styleElement) { 'applyStyles',
if (g_disableAll) { 'applyStyleState',
return; 'doDisableAll',
} 'initDocRewriteObserver',
iframe.srcdoc += styleElement.outerHTML; 'orphanCheck',
// make sure the style is added in case srcdoc was malformed 'removeStyle',
setTimeout(addStyleElement.bind(null, styleElement, iframe.contentDocument), 100); 'replaceAll',
} 'requestStyles',
// variables
function replaceAll(newStyles, doc, pass2) { 'ROOT',
var oldStyles = [].slice.call(doc.querySelectorAll("STYLE.stylus" + (pass2 ? "[id$='-ghost']" : ""))); 'disabledElements',
if (!pass2) { 'retiredStyleTimers',
oldStyles.forEach(function(style) { style.id += "-ghost"; }); 'styleElements',
} 'docRewriteObserver',
getDynamicIFrames(doc).forEach(function(iframe) { ].forEach(fn => (window[fn] = null));
replaceAll(newStyles, iframe.contentDocument, pass2);
});
if (doc == document && !pass2) {
g_styleElements = {};
applyStyles(newStyles);
replaceAll(newStyles, doc, true);
}
if (pass2) {
oldStyles.forEach(function(style) { style.remove(); });
}
}
// Observe dynamic IFRAMEs being added
function initObserver() {
var orphanCheckTimer;
iframeObserver = new MutationObserver(function(mutations) {
clearTimeout(orphanCheckTimer);
// MutationObserver runs as a microtask so the timer won't fire until all queued mutations are fired
orphanCheckTimer = setTimeout(orphanCheck, 0);
if (mutations.length > 1000) {
// use a much faster method for very complex pages with 100,000 mutations
// (observer usually receives 1k-10k mutations per call)
addDocumentStylesToAllIFrames();
return;
}
// move the check out of current execution context
// because some same-domain (!) iframes fail to load when their "contentDocument" is accessed (!)
// namely gmail's old chat iframe talkgadget.google.com
setTimeout(process.bind(null, mutations), 0);
});
function process(mutations) {
for (var m = 0, ml = mutations.length; m < ml; m++) {
var mutation = mutations[m];
if (mutation.type === "childList") {
for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) {
var node = nodes[n];
if (node.localName === "iframe" && iframeIsDynamic(node)) {
addDocumentStylesToIFrame(node);
}
}
}
}
}
iframeObserver.start = function() {
// will be ignored by browser if already observing
iframeObserver.observe(document, {childList: true, subtree: true});
}
function orphanCheck() {
orphanCheckTimer = 0;
var port = chrome.runtime.connect();
if (port) {
port.disconnect();
return;
}
// we're orphaned due to an extension update
// we can detach the mutation observer
iframeObserver.takeRecords();
iframeObserver.disconnect();
iframeObserver = null;
// we can detach event listeners
document.removeEventListener("DOMContentLoaded", onDOMContentLoaded);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
// we can destroy global functions in this context to free up memory
[
'addDocumentStylesToAllIFrames',
'addDocumentStylesToIFrame',
'addStyleElement',
'addStyleToIFrameSrcDoc',
'applyOnMessage',
'applySections',
'applyStyles',
'disableAll',
'getDynamicIFrames',
'iframeIsDynamic',
'iframeIsLoadingSrcDoc',
'initObserver',
'removeStyle',
'replaceAll',
'requestStyles',
'retireStyle'
].forEach(fn => window[fn] = null);
// we can destroy global variables
g_styleElements = iframeObserver = retiredStyleIds = null;
}
} }

View File

@ -1,236 +1,282 @@
/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */ /* global dbExec, getStyles, saveStyle */
'use strict';
var frameIdMessageable; // eslint-disable-next-line no-var
runTryCatch(function() { var browserCommands, contextMenus;
chrome.tabs.sendMessage(0, {}, {frameId: 0}, function() {
var clearError = chrome.runtime.lastError; // *************************************************************************
frameIdMessageable = true; // 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. // register all listeners
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, "styleApply")); chrome.runtime.onMessage.addListener(onRuntimeMessage);
// Not supported in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1239349
if ("onHistoryStateUpdated" in chrome.webNavigation) { chrome.webNavigation.onBeforeNavigate.addListener(data =>
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, "styleReplaceAll")); webNavigationListener(null, data));
}
chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null)); chrome.webNavigation.onCommitted.addListener(data =>
function webNavigationListener(method, data) { webNavigationListener('styleApply', data));
// Until Chrome 41, we can't target a frame with a message
// (https://developer.chrome.com/extensions/tabs#method-sendMessage) chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
// so a style affecting a page with an iframe will affect the main page as well. webNavigationListener('styleReplaceAll', data));
// Skip doing this for frames in pre-41 to prevent page flicker.
if (data.frameId != 0 && !frameIdMessageable) { chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
return; webNavigationListener('styleReplaceAll', data));
}
getStyles({matchUrl: data.url, enabled: true, asHash: true}, function(styleHash) { chrome.tabs.onAttached.addListener((tabId, data) => {
if (method) { // When an edit page gets attached or detached, remember its state
chrome.tabs.sendMessage(data.tabId, {method: method, styles: styleHash}, // so we can do the same to the next one to open.
frameIdMessageable ? {frameId: data.frameId} : undefined); chrome.tabs.get(tabId, tab => {
} if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) {
if (data.frameId == 0) { chrome.windows.get(tab.windowId, {populate: true}, win => {
updateIcon({id: data.tabId, url: data.url}, styleHash); // If there's only one tab in this window, it's been dragged to new window
} prefs.set('openEditInWindow', win.tabs.length == 1);
}); });
}
});
});
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
if ('commands' in chrome) {
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
chrome.commands.onCommand.addListener(command => browserCommands[command]());
} }
// catch direct URL hash modifications not invoked via HTML5 history API // *************************************************************************
var tabUrlHasHash = {}; {
chrome.tabs.onUpdated.addListener(function(tabId, info, tab) { const onInstall = ({reason}) => {
if (info.status == "loading" && info.url) { chrome.runtime.onInstalled.removeListener(onInstall);
if (info.url.indexOf('#') > 0) { const manifest = chrome.runtime.getManifest();
tabUrlHasHash[tabId] = true; // Open FAQs page once after installation to guide new users.
} else if (tabUrlHasHash[tabId]) { // Do not display it in development mode.
delete tabUrlHasHash[tabId]; if (reason == 'install' && manifest.update_url) {
} else { setTimeout(openURL, 100, {
// do nothing since the tab neither had # before nor has # now url: `http://add0n.com/stylus.html?version=${manifest.version}&type=install`
return; });
} }
webNavigationListener("styleReplaceAll", {tabId: tabId, frameId: 0, url: info.url}); // reset L10N cache on UI language change or update
} const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {};
}); const UIlang = chrome.i18n.getUILanguage();
chrome.tabs.onRemoved.addListener(function(tabId, info) { if (reason == 'update' || browserUIlanguage != UIlang) {
delete tabUrlHasHash[tabId]; localStorage.L10N = JSON.stringify({
}); browserUIlanguage: UIlang,
});
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { }
switch (request.method) { };
case "getStyles": // bind for 60 seconds max and auto-unbind if it's a normal run
var styles = getStyles(request, sendResponse); chrome.runtime.onInstalled.addListener(onInstall);
// check if this is a main content frame style enumeration setTimeout(onInstall, 60e3, {reason: 'unbindme'});
if (request.matchUrl && !request.id
&& sender && sender.tab && sender.frameId == 0
&& sender.tab.url == request.matchUrl) {
updateIcon(sender.tab, styles);
}
return KEEP_CHANNEL_OPEN;
case "saveStyle":
saveStyle(request).then(sendResponse);
return KEEP_CHANNEL_OPEN;
case "invalidateCache":
if (typeof invalidateCache != "undefined") {
invalidateCache(false);
}
break;
case "healthCheck":
getDatabase(function() { sendResponse(true); }, function() { sendResponse(false); });
return KEEP_CHANNEL_OPEN;
case "openURL":
openURL(request);
break;
case "styleDisableAll":
chrome.contextMenus.update("disableAll", {checked: request.disableAll});
break;
case "prefChanged":
if (request.prefName == "show-badge") {
chrome.contextMenus.update("show-badge", {checked: request.value});
}
else if (request.prefName === 'disableAll') {
chrome.contextMenus.update("disableAll", {checked: request.value});
}
break;
case "refreshAllTabs":
refreshAllTabs().then(sendResponse);
return KEEP_CHANNEL_OPEN;
}
});
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
if ("commands" in chrome) {
chrome.commands.onCommand.addListener(function(command) {
switch (command) {
case "openManage":
openURL({url: chrome.extension.getURL("manage.html")});
break;
case "styleDisableAll":
disableAllStylesToggle();
chrome.contextMenus.update("disableAll", {checked: prefs.get("disableAll")});
break;
}
});
} }
// 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. // browser commands
runTryCatch(function() { browserCommands = {
chrome.contextMenus.create({ openManage() {
id: "show-badge", title: chrome.i18n.getMessage("menuShowBadge"), openURL({url: '/manage.html'});
type: "checkbox", contexts: ["browser_action"], checked: prefs.get("show-badge") },
}, function() { var clearError = chrome.runtime.lastError }); styleDisableAll(info) {
chrome.contextMenus.create({ prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
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"), // context menus
type: "normal", contexts: ["browser_action"] contextMenus = Object.assign({
}, function() {var clearError = chrome.runtime.lastError}); 'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
click: browserCommands.openManage,
},
}, prefs.get('editor.contextDelete') && {
'editor.contextDelete': {
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'});
},
}
}); });
chrome.contextMenus.onClicked.addListener(function(info, tab) { {
if (info.menuItemId == "disableAll") { const createContextMenus = (ids = Object.keys(contextMenus)) => {
disableAllStylesToggle(info.checked); for (const id of ids) {
} const item = Object.assign({id}, contextMenus[id]);
else if (info.menuItemId === 'show-badge') { const prefValue = prefs.readOnlyValues[id];
prefs.set(info.menuItemId, info.checked); item.title = chrome.i18n.getMessage(item.title);
} if (!item.type && typeof prefValue == 'boolean') {
else if (info.menuItemId === 'open-manager') { item.type = 'checkbox';
openURL({url: chrome.extension.getURL("manage.html")}); item.checked = prefValue;
} }
}); if (!item.contexts) {
item.contexts = ['browser_action'];
function disableAllStylesToggle(newState) { }
if (newState === undefined || newState === null) { delete item.click;
newState = !prefs.get("disableAll"); chrome.contextMenus.create(item, ignoreChromeError);
} }
prefs.set("disableAll", newState); };
createContextMenus();
prefs.subscribe((id, checked) => {
if (id == 'editor.contextDelete') {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
} else {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
}
}, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean'));
} }
// Get the DB so that any first run actions will be performed immediately when the background page loads. // *************************************************************************
getDatabase(function() {}, reportError); // [re]inject content scripts
{
const NTP = 'chrome://newtab/';
const PING = {method: 'ping'};
const ALL_URLS = '<all_urls>';
const contentScripts = chrome.runtime.getManifest().content_scripts;
// expand * as .*?
const wildcardAsRegExp = (s, flags) => new RegExp(
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
.replace(/\*/g, '.*?'), flags);
for (const cs of contentScripts) {
cs.matches = cs.matches.map(m => (
m == ALL_URLS ? m : wildcardAsRegExp(m)
));
}
// When an edit page gets attached or detached, remember its state so we can do the same to the next one to open. const injectCS = (cs, tabId) => {
var editFullUrl = chrome.extension.getURL("edit.html"); chrome.tabs.executeScript(tabId, {
chrome.tabs.onAttached.addListener(function(tabId, data) { file: cs.js[0],
chrome.tabs.get(tabId, function(tabData) { runAt: cs.run_at,
if (tabData.url.indexOf(editFullUrl) == 0) { allFrames: cs.all_frames,
chrome.windows.get(tabData.windowId, {populate: true}, function(win) { matchAboutBlank: cs.match_about_blank,
// If there's only one tab in this window, it's been dragged to new window }, ignoreChromeError);
prefs.set("openEditInWindow", win.tabs.length == 1); };
});
}
});
});
function openURL(options) { const pingCS = (cs, {id, url}) => {
chrome.tabs.query({currentWindow: true, url: options.url}, function(tabs) { cs.matches.some(match => {
// switch to an existing tab with the requested url if ((match == ALL_URLS || url.match(match))
if (tabs.length) { && (!url.startsWith('chrome') || url == NTP)) {
chrome.tabs.highlight({windowId: tabs[0].windowId, tabs: tabs[0].index}, function (window) {}); chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id));
} else { return true;
delete options.method; }
getActiveTab(function(tab) { });
// re-use an active new tab page };
chrome.tabs[tab.url == "chrome://newtab/" ? "update" : "create"](options);
}); chrome.tabs.query({}, tabs =>
} tabs.forEach(tab =>
}); contentScripts.forEach(cs =>
pingCS(cs, tab))));
} }
var codeMirrorThemes;
getCodeMirrorThemes(function(themes) {
codeMirrorThemes = themes;
});
// do not use prefs.get('version', null) as it might not yet be available // *************************************************************************
chrome.storage.local.get('version', prefs => {
// Open FAQs page once after installation to guide new users,
// https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160
if (!prefs.version) {
// do not display the FAQs page in development mode
if ('update_url' in chrome.runtime.getManifest()) {
let version = chrome.runtime.getManifest().version;
chrome.storage.local.set({
version
}, () => {
window.setTimeout(() => {
chrome.tabs.create({
url: 'http://add0n.com/stylus.html?version=' + version + '&type=install'
});
}, 3000);
})
}
}
});
injectContentScripts(); function webNavigationListener(method, {url, tabId, frameId}) {
getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => {
function injectContentScripts() { if (method && !url.startsWith('chrome:') && tabId >= 0) {
const contentScripts = chrome.app.getDetails().content_scripts; chrome.tabs.sendMessage(tabId, {
for (let cs of contentScripts) { method,
cs.matches = cs.matches.map(m => m == '<all_urls>' ? m : wildcardAsRegExp(m)); // ping own page so it retrieves the styles directly
} styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
chrome.tabs.query({url: '*://*/*'}, tabs => { }, {
for (let tab of tabs) { frameId
for (let cs of contentScripts) { });
for (let m of cs.matches) { }
if (m == '<all_urls>' || tab.url.match(m)) { // main page frame id is 0
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { if (frameId == 0) {
if (!pong) { updateIcon({id: tabId, url}, styles);
chrome.tabs.executeScript(tab.id, { }
file: cs.js[0], });
runAt: cs.run_at, }
allFrames: cs.all_frames,
}, result => chrome.runtime.lastError); // ignore lastError just in case
} function updateIcon(tab, styles) {
}); if (tab.id < 0) {
// inject the content script just once return;
break; }
} 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'; 'use strict';
var STYLISH_DUMP_FILE_EXT = '.txt'; const STYLISH_DUMP_FILE_EXT = '.txt';
var STYLISH_DUMPFILE_EXTENSION = '.json'; const STYLUS_BACKUP_FILE_EXT = '.json';
var STYLISH_DEFAULT_SAVE_NAME = 'stylus-mm-dd-yyyy' + STYLISH_DUMP_FILE_EXT;
function importFromFile({fileTypeFilter, file} = {}) { function importFromFile({fileTypeFilter, file} = {}) {
return new Promise(resolve => { return new Promise(resolve => {
@ -25,7 +25,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
function readFile() { function readFile() {
if (file || fileInput.value !== fileInput.initialValue) { if (file || fileInput.value !== fileInput.initialValue) {
file = file || fileInput.files[0]; file = file || fileInput.files[0];
if (file.size > 100*1000*1000) { if (file.size > 100e6) {
console.warn("100MB backup? I don't believe you."); console.warn("100MB backup? I don't believe you.");
importFromString('').then(resolve); importFromString('').then(resolve);
return; return;
@ -45,103 +45,312 @@ function importFromFile({fileTypeFilter, file} = {}) {
}); });
} }
function importFromString(jsonString) { function importFromString(jsonString) {
const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || []; if (!BG) {
const numStyles = json.length; onBackgroundReady().then(() => importFromString(jsonString));
return;
if (numStyles) {
invalidateCache(true);
} }
// create objects in background context
const json = BG.tryJSONparse(jsonString) || [];
if (typeof json.slice != 'function') {
json.length = 0;
}
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
const oldStylesByName = json.length && new Map(
oldStyles.map(style => [style.name.trim(), style]));
return new Promise(resolve => { let oldDigests;
proceed(); chrome.storage.local.get(null, data => (oldDigests = data));
function proceed() {
const nextStyle = json.shift(); const stats = {
if (nextStyle) { added: {names: [], ids: [], legend: 'importReportLegendAdded'},
saveStyle(nextStyle, {notify: false}).then(style => { unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
handleUpdate(style); metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
setTimeout(proceed, 0); metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
}); codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
} else { invalid: {names: [], legend: 'importReportLegendInvalid'},
refreshAllTabs().then(() => { };
setTimeout(alert, 100, numStyles + ' styles installed/updated');
resolve(numStyles); let index = 0;
}); let lastRenderTime = performance.now();
const renderQueue = [];
const RENDER_NAP_TIME_MAX = 1000; // ms
const RENDER_QUEUE_MAX = 50; // number of styles
const SAVE_OPTIONS = {reason: 'import', notify: false};
return new Promise(proceed);
function proceed(resolve) {
while (index < json.length) {
const item = json[index++];
const info = analyze(item);
if (info) {
// using saveStyle directly since json was parsed in background page context
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
.then(style => account({style, info, resolve}));
} }
} }
}); renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
renderQueue.length = 0;
done(resolve);
}
function analyze(item) {
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|| (item.sections && typeof item.sections.slice != 'function')) {
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
return;
}
item.name = item.name.trim();
const byId = BG.cachedStyles.byId.get(item.id);
const byName = oldStylesByName.get(item.name);
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
if (oldStyle == byName && byName) {
item.id = byName.id;
}
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
const metaEqual = oldStyleKeys &&
oldStyleKeys.length == Object.keys(item).length &&
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
if (metaEqual && codeEqual) {
stats.unchanged.names.push(oldStyle.name);
stats.unchanged.ids.push(oldStyle.id);
return;
}
return {oldStyle, metaEqual, codeEqual};
}
function account({style, info, resolve}) {
renderQueue.push(style);
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|| renderQueue.length > RENDER_QUEUE_MAX) {
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
renderQueue.length = 0;
lastRenderTime = performance.now();
}
setTimeout(proceed, 0, resolve);
const {oldStyle, metaEqual, codeEqual} = info;
if (!oldStyle) {
stats.added.names.push(style.name);
stats.added.ids.push(style.id);
return;
}
if (!metaEqual && !codeEqual) {
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
stats.metaAndCode.ids.push(style.id);
return;
}
if (!codeEqual) {
stats.codeOnly.names.push(style.name);
stats.codeOnly.ids.push(style.id);
return;
}
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
stats.metaOnly.ids.push(style.id);
}
function done(resolve) {
const numChanged = stats.metaAndCode.names.length +
stats.metaOnly.names.length +
stats.codeOnly.names.length +
stats.added.names.length;
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
const report = Object.keys(stats)
.filter(kind => stats[kind].names.length)
.map(kind => {
const {ids, names, legend} = stats[kind];
const listItemsWithId = (name, i) =>
$element({dataset: {id: ids[i]}, textContent: name});
const listItems = name =>
$element({textContent: name});
const block =
$element({tag: 'details', dataset: {id: kind}, appendChild: [
$element({tag: 'summary', appendChild:
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
}),
$element({tag: 'small', appendChild:
names.map(ids ? listItemsWithId : listItems)
}),
]});
return block;
});
scrollTo(0, 0);
messageBox({
title: t('importReportTitle'),
contents: report.length ? report : t('importReportUnchanged'),
buttons: [t('confirmOK'), numChanged && t('undo')],
onshow: bindClick,
}).then(({button, enter, esc}) => {
if (button == 1) {
undo();
}
});
resolve(numChanged);
});
}
function undo() {
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
const newIds = [
...stats.metaAndCode.ids,
...stats.metaOnly.ids,
...stats.codeOnly.ids,
...stats.added.ids,
];
let resolve;
index = 0;
return new Promise(resolve_ => {
resolve = resolve_;
undoNextId();
}).then(BG.refreshAllTabs)
.then(() => messageBox({
title: t('importReportUndoneTitle'),
contents: newIds.length + ' ' + t('importReportUndone'),
buttons: [t('confirmOK')],
}));
function undoNextId() {
if (index == newIds.length) {
resolve();
return;
}
const id = newIds[index++];
deleteStyleSafe({id, notify: false}).then(id => {
const oldStyle = oldStylesById.get(id);
if (oldStyle) {
oldStyle.styleDigest = oldDigests[BG.DIGEST_KEY_PREFIX + id];
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
.then(undoNextId);
} else {
undoNextId();
}
});
}
}
function bindClick(box) {
const highlightElement = event => {
const styleElement = $('#style-' + event.target.dataset.id);
if (styleElement) {
scrollElementIntoView(styleElement);
animateElement(styleElement, {className: 'highlight'});
}
};
for (const block of $$('details')) {
if (block.dataset.id != 'invalid') {
block.style.cursor = 'pointer';
block.onclick = highlightElement;
}
}
}
function limitString(s, limit = 100) {
return s.length <= limit ? s : s.substr(0, limit) + '...';
}
function reportNameChange(oldStyle, newStyle) {
return newStyle.name != oldStyle.name
? oldStyle.name + ' —> ' + newStyle.name
: oldStyle.name;
}
function refreshAllTabs() {
return getActiveTab().then(activeTab => new Promise(resolve => {
// list all tabs including chrome-extension:// which can be ours
chrome.tabs.query({}, tabs => {
const lastTab = tabs[tabs.length - 1];
for (const tab of tabs) {
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
const message = {method: 'styleReplaceAll', styles};
if (tab.id == activeTab.id) {
applyOnMessage(message);
} else {
chrome.tabs.sendMessage(tab.id, message);
}
BG.updateIcon(tab, styles);
if (tab == lastTab) {
resolve();
}
});
}
});
}));
}
} }
function generateFileName() {
var today = new Date();
var dd = '0' + today.getDate();
var mm = '0' + (today.getMonth() + 1);
var yyyy = today.getFullYear();
dd = dd.substr(-2); $('#file-all-styles').onclick = () => {
mm = mm.substr(-2); Promise.all([
BG.chromeLocal.get(null),
today = mm + '-' + dd + '-' + yyyy; getStylesSafe(),
]).then(([data, styles]) => {
return 'stylus-' + today + STYLISH_DUMPFILE_EXTENSION; styles = styles.map(style => {
} const styleDigest = data[BG.DIGEST_KEY_PREFIX + style.id];
return styleDigest ? Object.assign({styleDigest}, style) : style;
document.getElementById('file-all-styles').onclick = () => { });
getStyles({}, function (styles) { const text = JSON.stringify(styles, null, '\t');
let text = JSON.stringify(styles, null, '\t'); const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
let fileName = generateFileName() || STYLISH_DEFAULT_SAVE_NAME; return url;
let url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
// for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600 // for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600
fetch(url) }).then(fetch)
.then(res => res.blob()) .then(res => res.blob())
.then(blob => { .then(blob => {
let a = document.createElement('a'); const objectURL = URL.createObjectURL(blob);
a.setAttribute('download', fileName); Object.assign(document.createElement('a'), {
a.setAttribute('href', URL.createObjectURL(blob)); download: generateFileName(),
a.dispatchEvent(new MouseEvent('click')); 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 = () => { $('#unfile-all-styles').onclick = () => {
importFromFile({fileTypeFilter: STYLISH_DUMPFILE_EXTENSION}); importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
}; };
const dropTarget = Object.assign(document.body, { Object.assign(document.body, {
ondragover: event => { ondragover(event) {
const hasFiles = event.dataTransfer.types.includes('Files'); const hasFiles = event.dataTransfer.types.includes('Files');
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none'; event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
dropTarget.classList.toggle('dropzone', hasFiles); this.classList.toggle('dropzone', hasFiles);
if (hasFiles) { if (hasFiles) {
event.preventDefault(); event.preventDefault();
clearTimeout(dropTarget.fadeoutTimer); clearTimeout(this.fadeoutTimer);
dropTarget.classList.remove('fadeout'); this.classList.remove('fadeout');
} }
}, },
ondragend: event => { ondragend(event) {
dropTarget.classList.add('fadeout'); animateElement(this, {className: 'fadeout'}).then(() => {
// transitionend event may not fire if the user switched to another tab so we'll use a timer this.style.animationDuration = '';
clearTimeout(dropTarget.fadeoutTimer); this.classList.remove('dropzone');
dropTarget.fadeoutTimer = setTimeout(() => { });
dropTarget.classList.remove('dropzone', 'fadeout');
}, 250);
}, },
ondragleave: event => { ondragleave(event) {
// Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds // Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds
if (!event.screenX && !event.screenX) { if (!event.screenX && !event.screenX) {
dropTarget.ondragend(); this.ondragend();
} }
}, },
ondrop: event => { ondrop(event) {
this.ondragend();
if (event.dataTransfer.files.length) { if (event.dataTransfer.files.length) {
event.preventDefault(); event.preventDefault();
importFromFile({file: event.dataTransfer.files[0]}).then(() => { if ($('#onlyUpdates input').checked) {
dropTarget.classList.remove('dropzone'); $('#onlyUpdates input').click();
}); }
} else { importFromFile({file: event.dataTransfer.files[0]});
dropTarget.ondragend();
} }
}, },
}); });

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) 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 Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files obtaining a copy of this software and associated documentation files
@ -39,13 +39,15 @@
css_beautify(source_text, options); css_beautify(source_text, options);
The options are (default in brackets): The options are (default in brackets):
indent_size (4) indentation size, indent_size (4) indentation size,
indent_char (space) character to indent with, indent_char (space) character to indent with,
selector_separator_newline (true) - separate selectors with newline or preserve_newlines (default false) - whether existing line breaks should be preserved,
not (e.g. "a,\nbr" or "a, br") selector_separator_newline (true) - separate selectors with newline or
end_with_newline (false) - end with a newline not (e.g. "a,\nbr" or "a, br")
newline_between_rules (true) - add a new line after every css rule 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 e.g
css_beautify(css_source_text, { css_beautify(css_source_text, {
@ -53,7 +55,8 @@
'indent_char': '\t', 'indent_char': '\t',
'selector_separator': ' ', 'selector_separator': ' ',
'end_with_newline': false, '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/ // http://www.w3.org/TR/css3-syntax/
(function() { (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 css_beautify(source_text, options) {
function defaultOption(opt, defaultValue) {
return opt === undefined ? defaultValue : opt;
}
options = options || {}; 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 // Allow the setting of language/file-type specific options
if (typeof indentSize === "string") { // with inheritance of overall settings
indentSize = parseInt(indentSize, 10); 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 // tokenizer
var whiteRe = /^\s+$/; var whiteRe = /^\s+$/;
var wordRe = /[\w$\-_]/;
var pos = -1, var pos = -1,
ch; ch;
@ -96,6 +138,7 @@
} }
function peek(skipWhitespace) { function peek(skipWhitespace) {
var result = '';
var prev_pos = pos; var prev_pos = pos;
if (skipWhitespace) { if (skipWhitespace) {
eatWhitespace(); eatWhitespace();
@ -128,12 +171,16 @@
return str; return str;
} }
function eatWhitespace() { function eatWhitespace(preserve_newlines_local) {
var result = ''; var result = 0;
while (whiteRe.test(peek())) { while (whiteRe.test(peek())) {
next(); next();
result += ch; if (ch === '\n' && preserve_newlines_local && preserve_newlines) {
print.newLine(true);
result++;
}
} }
newlinesFromLastWSEat = result;
return result; return result;
} }
@ -174,11 +221,20 @@
// and the next special character found opens // and the next special character found opens
// a new block // a new block
function foundNestedPseudoClass() { function foundNestedPseudoClass() {
var openParen = 0;
for (var i = pos + 1; i < source_text.length; i++) { for (var i = pos + 1; i < source_text.length; i++) {
var ch = source_text.charAt(i); var ch = source_text.charAt(i);
if (ch === "{") { if (ch === "{") {
return true; 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; return false;
} }
} }
@ -203,14 +259,20 @@
var print = {}; var print = {};
print["{"] = function(ch) { print["{"] = function(ch) {
newline_before_open_brace ? output.push('\n') : print.singleSpace(); print.singleSpace();
output.push(ch); output.push(ch);
newline_after_open_brace ? print.newLine() : print.singleSpace(); if (!eatWhitespace(true)) {
print.newLine();
}
}; };
print["}"] = function(ch) { print["}"] = function(newline) {
newline_before_close_brace ? print.newLine() : print.singleSpace(); if (newline) {
output.push(ch); print.newLine();
print.newLine(); }
output.push('}');
if (!eatWhitespace(true)) {
print.newLine();
}
}; };
print._lastCharWhitespace = function() { print._lastCharWhitespace = function() {
@ -218,15 +280,17 @@
}; };
print.newLine = function(keepWhitespace) { print.newLine = function(keepWhitespace) {
if (!keepWhitespace) {
print.trim();
}
if (output.length) { if (output.length) {
if (!keepWhitespace && output[output.length - 1] !== '\n') {
print.trim();
} else if (output[output.length - 1] === basebaseIndentString) {
output.pop();
}
output.push('\n'); output.push('\n');
}
if (basebaseIndentString) { if (basebaseIndentString) {
output.push(basebaseIndentString); output.push(basebaseIndentString);
}
} }
}; };
print.singleSpace = function() { print.singleSpace = function() {
@ -235,6 +299,12 @@
} }
}; };
print.preserveSingleSpace = function() {
if (isAfterSpace) {
print.singleSpace();
}
};
print.trim = function() { print.trim = function() {
while (print._lastCharWhitespace()) { while (print._lastCharWhitespace()) {
output.pop(); output.pop();
@ -243,12 +313,10 @@
var output = []; var output = [];
if (basebaseIndentString) {
output.push(basebaseIndentString);
}
/*_____________________--------------------_____________________*/ /*_____________________--------------------_____________________*/
var insideRule = false; var insideRule = false;
var insidePropertyValue = false;
var enteringConditionalGroup = false; var enteringConditionalGroup = false;
var top_ch = ''; var top_ch = '';
var last_top_ch = ''; var last_top_ch = '';
@ -263,8 +331,12 @@
if (!ch) { if (!ch) {
break; break;
} else if (ch === '/' && peek() === '*') { /* css comment */ } else if (ch === '/' && peek() === '*') { /* css comment */
var header = lookBack(""); var header = indentLevel === 0;
print.newLine();
if (isAfterNewline || header) {
print.newLine();
}
output.push(eatComment()); output.push(eatComment());
print.newLine(); print.newLine();
if (header) { if (header) {
@ -278,40 +350,46 @@
output.push(eatComment()); output.push(eatComment());
print.newLine(); print.newLine();
} else if (ch === '@') { } else if (ch === '@') {
// pass along the space we found as a separate item print.preserveSingleSpace();
if (isAfterSpace) {
print.singleSpace();
}
output.push(ch);
// strip trailing space, if present, for hash property checks // deal with less propery mixins @{...}
var variableOrRule = peekString(": ,;{}()[]/='\""); if (peek() === '{') {
output.push(eatString('}'));
} else {
output.push(ch);
if (variableOrRule.match(/[ :]$/)) { // strip trailing space, if present, for hash property checks
// we have a variable or pseudo-class, add it and insert one space before continuing var variableOrRule = peekString(": ,;{}()[]/='\"");
next();
variableOrRule = eatString(": ").replace(/\s$/, '');
output.push(variableOrRule);
print.singleSpace();
}
variableOrRule = variableOrRule.replace(/\s$/, '') if (variableOrRule.match(/[ :]$/)) {
// we have a variable or pseudo-class, add it and insert one space before continuing
next();
variableOrRule = eatString(": ").replace(/\s$/, '');
output.push(variableOrRule);
print.singleSpace();
}
// might be a nesting at-rule variableOrRule = variableOrRule.replace(/\s$/, '');
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
nestedLevel += 1; // might be a nesting at-rule
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) { if (variableOrRule in css_beautify.NESTED_AT_RULE) {
enteringConditionalGroup = true; nestedLevel += 1;
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
enteringConditionalGroup = true;
}
} }
} }
} else if (ch === '#' && peek() === '{') {
print.preserveSingleSpace();
output.push(eatString('}'));
} else if (ch === '{') { } else if (ch === '{') {
if (peek(true) === '}') { if (peek(true) === '}') {
eatWhitespace(); eatWhitespace();
next(); next();
print.singleSpace(); print.singleSpace();
output.push("{}"); output.push("{");
print.newLine(); print['}'](false);
if (newline_between_rules && indentLevel === 0) { if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
print.newLine(true); print.newLine(true);
} }
} else { } else {
@ -328,25 +406,35 @@
} }
} else if (ch === '}') { } else if (ch === '}') {
outdent(); outdent();
print["}"](ch); print["}"](true);
insideRule = false; insideRule = false;
insidePropertyValue = false;
if (nestedLevel) { if (nestedLevel) {
nestedLevel--; nestedLevel--;
} }
if (newline_between_rules && indentLevel === 0) { if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
print.newLine(true); print.newLine(true);
} }
} else if (ch === ":") { } else if (ch === ":") {
eatWhitespace(); eatWhitespace();
if ((insideRule || enteringConditionalGroup) && if ((insideRule || enteringConditionalGroup) &&
!(lookBack("&") || foundNestedPseudoClass())) { !(lookBack("&") || foundNestedPseudoClass()) &&
!lookBack("(")) {
// 'property: value' delimiter // 'property: value' delimiter
// which could be in a conditional group query // which could be in a conditional group query
output.push(':'); output.push(':');
print.singleSpace(); if (!insidePropertyValue) {
insidePropertyValue = true;
print.singleSpace();
}
} else { } else {
// sass/less parent reference don't use a space // sass/less parent reference don't use a space
// sass nested pseudo-class 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() === ":") { if (peek() === ":") {
// pseudo-element // pseudo-element
next(); next();
@ -357,13 +445,14 @@
} }
} }
} else if (ch === '"' || ch === '\'') { } else if (ch === '"' || ch === '\'') {
if (isAfterSpace) { print.preserveSingleSpace();
print.singleSpace();
}
output.push(eatString(ch)); output.push(eatString(ch));
} else if (ch === ';') { } else if (ch === ';') {
insidePropertyValue = false;
output.push(ch); output.push(ch);
newline_between_properties ? print.newLine() : print.singleSpace(); if (!eatWhitespace(true)) {
print.newLine();
}
} else if (ch === '(') { // may be a url } else if (ch === '(') { // may be a url
if (lookBack("url")) { if (lookBack("url")) {
output.push(ch); output.push(ch);
@ -377,9 +466,7 @@
} }
} else { } else {
parenLevel++; parenLevel++;
if (isAfterSpace) { print.preserveSingleSpace();
print.singleSpace();
}
output.push(ch); output.push(ch);
eatWhitespace(); eatWhitespace();
} }
@ -388,38 +475,58 @@
parenLevel--; parenLevel--;
} else if (ch === ',') { } else if (ch === ',') {
output.push(ch); output.push(ch);
eatWhitespace(); if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) {
if (!insideRule && selectorSeparatorNewline && parenLevel < 1) {
print.newLine(); print.newLine();
} else { } else {
print.singleSpace(); 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 === ']') { } else if (ch === ']') {
output.push(ch); output.push(ch);
} else if (ch === '[') { } else if (ch === '[') {
if (isAfterSpace) { print.preserveSingleSpace();
print.singleSpace();
}
output.push(ch); output.push(ch);
} else if (ch === '=') { // no whitespace before or after } else if (ch === '=') { // no whitespace before or after
eatWhitespace() eatWhitespace();
ch = '='; output.push('=');
output.push(ch); if (whiteRe.test(ch)) {
} else { ch = '';
if (isAfterSpace) {
print.singleSpace();
} }
} else {
print.preserveSingleSpace();
output.push(ch); 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 // establish end_with_newline
if (end_with_newline) { if (end_with_newline) {
sweetCode += "\n"; sweetCode += '\n';
}
if (eol !== '\n') {
sweetCode = sweetCode.replace(/[\n]/g, eol);
} }
return sweetCode; return sweetCode;
@ -461,4 +568,4 @@
global.css_beautify = css_beautify; global.css_beautify = css_beautify;
} }
}()); }());

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 Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy 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 // Rough heuristic to try and detect lines that are part of multi-line string
function probablyInsideString(cm, pos, line) { 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) { CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions; if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from); var self = this, mode = getMode(self, from);
var firstLine = self.getLine(from.line); var firstLine = self.getLine(from.line);
if (firstLine == null || probablyInsideString(self, from, firstLine)) return; if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
@ -95,7 +100,7 @@
CodeMirror.defineExtension("blockComment", function(from, to, options) { CodeMirror.defineExtension("blockComment", function(from, to, options) {
if (!options) options = noOptions; 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 startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd; var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) { if (!startString || !endString) {
@ -129,7 +134,7 @@
CodeMirror.defineExtension("uncomment", function(from, to, options) { CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions; 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); 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 // Try finding line comments
@ -171,9 +176,11 @@
endLine = self.getLine(--end); endLine = self.getLine(--end);
close = endLine.indexOf(endString); close = endLine.indexOf(endString);
} }
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
if (close == -1 || if (close == -1 ||
!/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) || !/comment/.test(self.getTokenTypeAt(insideStart)) ||
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1)))) !/comment/.test(self.getTokenTypeAt(insideEnd)) ||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
return false; return false;
// Avoid killing block comments completely outside the selection. // Avoid killing block comments completely outside the selection.

View File

@ -140,7 +140,11 @@
if (options.async || getAnnotations.async) { if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations, passOptions) lintAsync(cm, getAnnotations, passOptions)
} else { } 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; curLine = pos.line;
curLineObj = cm.getLineHandle(curLine); 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"]; return cm.charCoords(pos, "local")[top ? "top" : "bottom"];
var topY = cm.heightAtLine(curLineObj, "local"); var topY = cm.heightAtLine(curLineObj, "local");
return topY + (top ? 0 : curLineObj.height); return topY + (top ? 0 : curLineObj.height);
} }
var lastLine = cm.lastLine()
if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
var ann = anns[i]; var ann = anns[i];
if (ann.to.line > lastLine) continue;
var top = nextTop || getY(ann.from, true) * hScale; var top = nextTop || getY(ann.from, true) * hScale;
var bottom = getY(ann.to, false) * hScale; var bottom = getY(ann.to, false) * hScale;
while (i < anns.length - 1) { while (i < anns.length - 1) {
if (anns[i + 1].to.line > lastLine) break;
nextTop = getY(anns[i + 1].from, true) * hScale; nextTop = getY(anns[i + 1].from, true) * hScale;
if (nextTop > bottom + .9) break; if (nextTop > bottom + .9) break;
ann = anns[++i]; ann = anns[++i];

View File

@ -371,7 +371,9 @@
"Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd", "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
"Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace", "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
"Alt-/": "autocomplete", "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) { "Alt-G G": function(cm) {
var prefix = getPrefix(cm, true); var prefix = getPrefix(cm, true);

View File

@ -152,18 +152,25 @@
var text = cm.getRange(from, to); var text = cm.getRange(from, to);
var query = fullWord ? new RegExp("\\b" + text + "\\b") : text; var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
var cur = cm.getSearchCursor(query, to); var cur = cm.getSearchCursor(query, to);
if (cur.findNext()) { var found = cur.findNext();
cm.addSelection(cur.from(), cur.to()); if (!found) {
} else {
cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0)); cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
if (cur.findNext()) found = cur.findNext();
cm.addSelection(cur.from(), cur.to());
} }
if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to()))
return CodeMirror.Pass
cm.addSelection(cur.from(), cur.to());
} }
if (fullWord) if (fullWord)
cm.state.sublimeFindFullWord = cm.doc.sel; 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 = "(){}[]"; var mirror = "(){}[]";
function selectBetweenBrackets(cm) { function selectBetweenBrackets(cm) {
var ranges = cm.listSelections(), newRanges = [] 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: '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: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
{ keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, { 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: '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: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
{ keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
@ -1245,11 +1245,13 @@
} }
} }
function onPromptKeyUp(e, query, close) { function onPromptKeyUp(e, query, close) {
var keyName = CodeMirror.keyName(e), up; var keyName = CodeMirror.keyName(e), up, offset;
if (keyName == 'Up' || keyName == 'Down') { if (keyName == 'Up' || keyName == 'Down') {
up = keyName == 'Up' ? true : false; up = keyName == 'Up' ? true : false;
offset = e.target ? e.target.selectionEnd : 0;
query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
close(query); close(query);
if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length);
} else { } else {
if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
vimGlobalState.searchHistoryController.reset(); vimGlobalState.searchHistoryController.reset();
@ -1281,6 +1283,8 @@
clearInputState(cm); clearInputState(cm);
close(); close();
cm.focus(); cm.focus();
} else if (keyName == 'Up' || keyName == 'Down') {
CodeMirror.e_stop(e);
} else if (keyName == 'Ctrl-U') { } else if (keyName == 'Ctrl-U') {
// Ctrl-U clears input. // Ctrl-U clears input.
CodeMirror.e_stop(e); CodeMirror.e_stop(e);
@ -1344,7 +1348,7 @@
exCommandDispatcher.processCommand(cm, input); exCommandDispatcher.processCommand(cm, input);
} }
function onPromptKeyDown(e, input, close) { 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-[' || if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
(keyName == 'Backspace' && input == '')) { (keyName == 'Backspace' && input == '')) {
vimGlobalState.exCommandHistoryController.pushInput(input); vimGlobalState.exCommandHistoryController.pushInput(input);
@ -1355,9 +1359,12 @@
cm.focus(); cm.focus();
} }
if (keyName == 'Up' || keyName == 'Down') { if (keyName == 'Up' || keyName == 'Down') {
CodeMirror.e_stop(e);
up = keyName == 'Up' ? true : false; up = keyName == 'Up' ? true : false;
offset = e.target ? e.target.selectionEnd : 0;
input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
close(input); close(input);
if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length);
} else if (keyName == 'Ctrl-U') { } else if (keyName == 'Ctrl-U') {
// Ctrl-U clears input. // Ctrl-U clears input.
CodeMirror.e_stop(e); CodeMirror.e_stop(e);
@ -1620,9 +1627,8 @@
return findNext(cm, prev/** prev */, query, motionArgs.repeat); return findNext(cm, prev/** prev */, query, motionArgs.repeat);
}, },
goToMark: function(cm, _head, motionArgs, vim) { goToMark: function(cm, _head, motionArgs, vim) {
var mark = vim.marks[motionArgs.selectedCharacter]; var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter);
if (mark) { if (pos) {
var pos = mark.find();
return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
} }
return null; return null;
@ -3966,6 +3972,17 @@
return {top: from.line, bottom: to.line}; 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() { var ExCommandDispatcher = function() {
this.buildCommandMap_(); this.buildCommandMap_();
}; };
@ -4074,11 +4091,10 @@
case '$': case '$':
return cm.lastLine(); return cm.lastLine();
case '\'': case '\'':
var mark = cm.state.vim.marks[inputStream.next()]; var markName = inputStream.next();
if (mark && mark.find()) { var markPos = getMarkPos(cm, cm.state.vim, markName);
return mark.find().line; if (!markPos) throw new Error('Mark not set');
} return markPos.line;
throw new Error('Mark not set');
default: default:
inputStream.backUp(1); inputStream.backUp(1);
return undefined; return undefined;
@ -4147,8 +4163,8 @@
var mapping = { var mapping = {
keys: lhs, keys: lhs,
type: 'keyToEx', type: 'keyToEx',
exArgs: { input: rhs.substring(1) }, exArgs: { input: rhs.substring(1) }
user: true}; };
if (ctx) { mapping.context = ctx; } if (ctx) { mapping.context = ctx; }
defaultKeymap.unshift(mapping); defaultKeymap.unshift(mapping);
} else { } else {
@ -4156,8 +4172,7 @@
var mapping = { var mapping = {
keys: lhs, keys: lhs,
type: 'keyToKey', type: 'keyToKey',
toKeys: rhs, toKeys: rhs
user: true
}; };
if (ctx) { mapping.context = ctx; } if (ctx) { mapping.context = ctx; }
defaultKeymap.unshift(mapping); defaultKeymap.unshift(mapping);
@ -4178,8 +4193,7 @@
var keys = lhs; var keys = lhs;
for (var i = 0; i < defaultKeymap.length; i++) { for (var i = 0; i < defaultKeymap.length; i++) {
if (keys == defaultKeymap[i].keys if (keys == defaultKeymap[i].keys
&& defaultKeymap[i].context === ctx && defaultKeymap[i].context === ctx) {
&& defaultKeymap[i].user) {
defaultKeymap.splice(i, 1); defaultKeymap.splice(i, 1);
return; return;
} }
@ -4310,25 +4324,27 @@
showConfirm(cm, regInfo); showConfirm(cm, regInfo);
}, },
sort: function(cm, params) { sort: function(cm, params) {
var reverse, ignoreCase, unique, number; var reverse, ignoreCase, unique, number, pattern;
function parseArgs() { function parseArgs() {
if (params.argString) { if (params.argString) {
var args = new CodeMirror.StringStream(params.argString); var args = new CodeMirror.StringStream(params.argString);
if (args.eat('!')) { reverse = true; } if (args.eat('!')) { reverse = true; }
if (args.eol()) { return; } if (args.eol()) { return; }
if (!args.eatSpace()) { return 'Invalid arguments'; } if (!args.eatSpace()) { return 'Invalid arguments'; }
var opts = args.match(/[a-z]+/); var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/);
if (opts) { if (!opts && !args.eol()) { return 'Invalid arguments'; }
opts = opts[0]; if (opts[1]) {
ignoreCase = opts.indexOf('i') != -1; ignoreCase = opts[1].indexOf('i') != -1;
unique = opts.indexOf('u') != -1; unique = opts[1].indexOf('u') != -1;
var decimal = opts.indexOf('d') != -1 && 1; var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1;
var hex = opts.indexOf('x') != -1 && 1; var hex = opts[1].indexOf('x') != -1 && 1;
var octal = opts.indexOf('o') != -1 && 1; var octal = opts[1].indexOf('o') != -1 && 1;
if (decimal + hex + octal > 1) { return 'Invalid arguments'; } if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; 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(); var err = parseArgs();
@ -4342,14 +4358,18 @@
var curStart = Pos(lineStart, 0); var curStart = Pos(lineStart, 0);
var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
var text = cm.getRange(curStart, curEnd).split('\n'); 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 == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
(number == 'octal') ? /([0-7]+)/ : null; (number == 'octal') ? /([0-7]+)/ : null;
var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
var numPart = [], textPart = []; var numPart = [], textPart = [];
if (number) { if (number || pattern) {
for (var i = 0; i < text.length; i++) { 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]); numPart.push(text[i]);
} else { } else {
textPart.push(text[i]); textPart.push(text[i]);
@ -4368,8 +4388,17 @@
bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
return anum - bnum; return anum - bnum;
} }
numPart.sort(compareFn); function comparePatternFn(a, b) {
textPart.sort(compareFn); 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); text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
if (unique) { // Remove duplicate lines if (unique) { // Remove duplicate lines
var textOld = text; var textOld = text;

View File

@ -223,11 +223,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
cursor: default; cursor: default;
z-index: 4; z-index: 4;
} }
.CodeMirror-gutter-wrapper { .CodeMirror-gutter-wrapper ::selection { background-color: transparent }
-webkit-user-select: none; .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
-moz-user-select: none;
user-select: none;
}
.CodeMirror-lines { .CodeMirror-lines {
cursor: text; cursor: text;
@ -272,6 +269,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-widget {} .CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code { .CodeMirror-code {
outline: none; outline: none;
} }

View File

@ -28,6 +28,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
colorKeywords = parserConfig.colorKeywords || {}, colorKeywords = parserConfig.colorKeywords || {},
valueKeywords = parserConfig.valueKeywords || {}, valueKeywords = parserConfig.valueKeywords || {},
allowNested = parserConfig.allowNested, allowNested = parserConfig.allowNested,
lineComment = parserConfig.lineComment,
supportsAtComponent = parserConfig.supportsAtComponent === true; supportsAtComponent = parserConfig.supportsAtComponent === true;
var type, override; var type, override;
@ -253,6 +254,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
}; };
states.pseudo = function(type, stream, state) { states.pseudo = function(type, stream, state) {
if (type == "meta") return "pseudo";
if (type == "word") { if (type == "word") {
override = "variable-3"; override = "variable-3";
return state.context.type; return state.context.type;
@ -407,6 +410,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
electricChars: "}", electricChars: "}",
blockCommentStart: "/*", blockCommentStart: "/*",
blockCommentEnd: "*/", blockCommentEnd: "*/",
lineComment: lineComment,
fold: "brace" fold: "brace"
}; };
}); });
@ -663,7 +667,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", "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", "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", "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-caption", "table-cell", "table-column", "table-column-group",
"table-footer-group", "table-header-group", "table-row", "table-row-group", "table-footer-group", "table-header-group", "table-row", "table-row-group",
"tamil", "tamil",
@ -730,6 +734,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
valueKeywords: valueKeywords, valueKeywords: valueKeywords,
fontProperties: fontProperties, fontProperties: fontProperties,
allowNested: true, allowNested: true,
lineComment: "//",
tokenHooks: { tokenHooks: {
"/": function(stream, state) { "/": function(stream, state) {
if (stream.eat("/")) { if (stream.eat("/")) {
@ -772,6 +777,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
valueKeywords: valueKeywords, valueKeywords: valueKeywords,
fontProperties: fontProperties, fontProperties: fontProperties,
allowNested: true, allowNested: true,
lineComment: "//",
tokenHooks: { tokenHooks: {
"/": function(stream, state) { "/": function(stream, state) {
if (stream.eat("/")) { if (stream.eat("/")) {

View File

@ -64,7 +64,7 @@ code {
</textarea></form> </textarea></form>
<script> <script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), { var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
extraKeys: {"Ctrl-Space": "autocomplete"}, extraKeys: {"Ctrl-Space": "autocomplete"}
}); });
</script> </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(); //functionText += this._term();
lt = tokenStream.peek(); 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(); tokenStream.get();
functionText += tokenStream.token().value; functionText += tokenStream.token().value;
lt = tokenStream.peek(); 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;
}

182
edit.html
View File

@ -1,6 +1,14 @@
<html> <html id="stylus">
<head> <head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <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> <script src="codemirror/lib/codemirror.js"></script>
<link rel="stylesheet" href="codemirror/lib/codemirror.css"> <link rel="stylesheet" href="codemirror/lib/codemirror.css">
<script src="codemirror/mode/css/css.js"></script> <script src="codemirror/mode/css/css.js"></script>
@ -9,6 +17,7 @@
<link rel="stylesheet" href="codemirror/addon/search/matchesonscrollbar.css"> <link rel="stylesheet" href="codemirror/addon/search/matchesonscrollbar.css">
<script src="codemirror/addon/scroll/annotatescrollbar.js"></script> <script src="codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="codemirror/addon/search/matchesonscrollbar.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/dialog/dialog.js"></script>
<script src="codemirror/addon/search/searchcursor.js"></script> <script src="codemirror/addon/search/searchcursor.js"></script>
<script src="codemirror/addon/search/search.js"></script> <script src="codemirror/addon/search/search.js"></script>
@ -40,27 +49,29 @@
body { body {
margin: 0; margin: 0;
font: 9pt arial,sans-serif; font: 12px arial,sans-serif;
} }
/************ header ************/ /************ header ************/
#header { #header {
height: calc(100vh - 30px); width: 280px;
height: 100vh;
overflow: auto; overflow: auto;
width: 15rem;
position: fixed; position: fixed;
top: 0; top: 0;
padding: 0.95rem; padding: 15px;
border-right: 1px dashed #AAA; border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 3rem -1.2rem black; -webkit-box-shadow: 0 0 3rem -1.2rem black;
box-sizing: border-box;
} }
#header h1 { #header h1 {
margin-top: 0; margin-top: 0;
} }
#sections { #sections {
padding-left: 18rem; padding-left: 280px;
} }
#sections h2 { #sections h2 {
margin-top: 0.5rem; margin-top: 1rem;
margin-left: 1.7rem;
} }
.aligned { .aligned {
display: table-row; display: table-row;
@ -90,23 +101,39 @@
#url:not([href^="http"]) { #url:not([href^="http"]) {
display: none; display: none;
} }
#save-button {
opacity: .5;
pointer-events: none;
}
.dirty #save-button {
opacity: 1;
pointer-events: all;
}
.svg-icon { .svg-icon {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
transition: fill .5s; transition: fill .5s;
width: 16px;
height: 16px;
} }
.svg-icon:not(.applies-to-help):not(.dismiss) { .svg-icon:not(.dismiss) {
margin-left: 0.2rem; margin-left: 0.2rem;
} }
h2 .svg-icon, label .svg-icon { h2 .svg-icon, label .svg-icon {
margin-top: -2px; margin-top: -2px;
} }
.svg-icon.info:hover { .svg-icon.info {
fill: #000000; width: 14px;
height: 16px;
}
.svg-icon:hover,
.svg-icon.info {
fill: #666;
}
.svg-icon,
.svg-icon.info:hover {
fill: #000;
} }
a:hover .svg-icon.installed, .svg-icon.dismiss:hover {
fill: hsl(0, 0%, 40%);
}
#enabled { #enabled {
margin-left: 0; margin-left: 0;
vertical-align: middle; vertical-align: middle;
@ -183,6 +210,15 @@
.CodeMirror-search-hint { .CodeMirror-search-hint {
color: #888; 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 { @-webkit-keyframes highlight {
from { from {
background-color: #ff9; background-color: #ff9;
@ -191,6 +227,18 @@
background-color: none; 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 { .resize-grip {
position: absolute; position: absolute;
display: block; display: block;
@ -245,6 +293,62 @@
.applies-to img { .applies-to img {
vertical-align: bottom; 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 ************/
#help-popup { #help-popup {
top: 3rem; top: 3rem;
@ -269,16 +373,16 @@
font-weight: bold; font-weight: bold;
background-color: rgba(0,0,0,0.05); background-color: rgba(0,0,0,0.05);
margin: -0.5rem -0.5rem 0.5rem; margin: -0.5rem -0.5rem 0.5rem;
padding: 0.5rem; padding: .5rem 32px .5rem .5rem;
} }
#help-popup .contents { #help-popup .contents {
max-height: calc(100vh - 8rem); max-height: calc(100vh - 8rem);
overflow-y: auto; overflow-y: auto;
} }
#help-popup .close-icon { #help-popup .dismiss {
position: absolute; position: absolute;
right: 4px; right: 4px;
top: 4px; top: .5em;
} }
.keymap-list { .keymap-list {
@ -514,13 +618,14 @@
<br> <br>
<div class="applies-to"> <div class="applies-to">
<label i18n-text="appliesLabel"> <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> </label>
<ul class="applies-to-list"></ul> <ul class="applies-to-list"></ul>
</div> </div>
<button class="remove-section" i18n-text="sectionRemove"></button> <button class="remove-section" i18n-text="sectionRemove"></button>
<button class="add-section" i18n-text="sectionAdd"></button> <button class="add-section" i18n-text="sectionAdd"></button>
<button class="beautify-section" i18n-text="styleBeautify"></button> <button class="beautify-section" i18n-text="styleBeautify"></button>
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
</div> </div>
</template> </template>
<template data-id="find"> <template data-id="find">
@ -552,20 +657,18 @@
<template data-id="jumpToLine"> <template data-id="jumpToLine">
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span> <span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
</template> </template>
<template data-id="regexpTestPartial">
<script src="storage.js"></script> <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>
<script src="messaging.js"></script> </template>
<script src="localization.js"></script>
<script src="apply.js"></script>
<script src="edit.js"></script>
</head> </head>
<body id="stylus-edit"> <body id="stylus-edit">
<div id="header"> <div id="header">
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift --> <h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info"> <section id="basic-info">
<div id="basic-info-name"> <div id="basic-info-name">
<input id="name" class="style-contributor" i18n-placeholder="styleMissingName"> <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>
<div id="basic-info-enabled"> <div id="basic-info-enabled">
<input type="checkbox" id="enabled" class="style-contributor"> <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> <a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div> </div>
<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="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button> <button id="to-mozilla" i18n-text="exportLabel"></button>
</div> </div>
@ -605,21 +708,42 @@
<div class="option aligned"> <div class="option aligned">
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label> <label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
<select data-option="keyMap" id="editor.keyMap"></select> <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>
<div class="option aligned"> <div class="option aligned">
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label> <label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
<select data-option="theme" id="editor.theme"></select> <select data-option="theme" id="editor.theme"></select>
</div> </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>
<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> </div>
<section id="sections"> <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> </section>
<div id="help-popup"> <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 class="contents"></div>
</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> </body>
</html> </html>

472
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"; "use strict";
var styleId = null; 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 propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"};
var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"}; 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 // make querySelectorAll enumeration code readable
["forEach", "some", "indexOf", "map"].forEach(function(method) { ["forEach", "some", "indexOf", "map"].forEach(function(method) {
NodeList.prototype[method]= Array.prototype[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); var r = this.slice(-amount, this.length);
Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
return r; return r;
} };
Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }}); 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 // reroute handling to nearest editor when keypress resolves to one of these commands
var hotkeyRerouter = { var hotkeyRerouter = {
commands: { commands: {
@ -130,6 +146,11 @@ function initCodeMirror() {
var CM = CodeMirror; var CM = CodeMirror;
var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0; 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 // default option values
Object.assign(CM.defaults, { Object.assign(CM.defaults, {
mode: 'css', mode: 'css',
@ -138,6 +159,7 @@ function initCodeMirror() {
foldGutter: true, foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"], gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
matchBrackets: true, matchBrackets: true,
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")}, lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")},
lintReportDelay: prefs.get("editor.lintReportDelay"), lintReportDelay: prefs.get("editor.lintReportDelay"),
styleActiveLine: true, styleActiveLine: true,
@ -229,38 +251,30 @@ function initCodeMirror() {
return this.display.wrapper.parentNode; 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 // initialize global editor controls
document.addEventListener("DOMContentLoaded", function() { function optionsHtmlFromArray(options) {
function optionsHtmlFromArray(options) { return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join("");
return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join(""); }
} var themeControl = document.getElementById("editor.theme");
var themeControl = document.getElementById("editor.theme"); const themeList = localStorage.codeMirrorThemes;
var bg = chrome.extension.getBackgroundPage(); if (themeList) {
if (bg && bg.codeMirrorThemes) { themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/));
themeControl.innerHTML = optionsHtmlFromArray(bg.codeMirrorThemes); } else {
} else { // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
// 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]); themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
getCodeMirrorThemes(function(themes) { getCodeMirrorThemes().then(() => {
themeControl.innerHTML = optionsHtmlFromArray(themes); const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); 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); document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort());
setupLivePrefs( document.getElementById("options").addEventListener("change", acmeEventListener, false);
document.querySelectorAll("#options *[data-option][id^='editor.']") setupLivePrefs();
.map(function(option) { return option.id })
);
});
hotkeyRerouter.setState(true); hotkeyRerouter.setState(true);
} }
initCodeMirror();
function acmeEventListener(event) { function acmeEventListener(event) {
var el = event.target; var el = event.target;
@ -287,7 +301,7 @@ function acmeEventListener(event) {
el.selectedIndex = 0; el.selectedIndex = 0;
break; 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() if (themeLink.href == url) { // preloaded in initCodeMirror()
break; break;
} }
@ -302,13 +316,23 @@ function acmeEventListener(event) {
}, 100); }, 100);
})(); })();
return; 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); CodeMirror.setOption(option, value);
} }
// replace given textarea with the CodeMirror editor // replace given textarea with the CodeMirror editor
function setupCodeMirror(textarea, index) { function setupCodeMirror(textarea, index) {
var cm = CodeMirror.fromTextArea(textarea); var cm = CodeMirror.fromTextArea(textarea, {lint: null});
cm.on("change", indicateCodeChange); cm.on("change", indicateCodeChange);
cm.on("blur", function(cm) { cm.on("blur", function(cm) {
@ -324,6 +348,7 @@ function setupCodeMirror(textarea, index) {
hotkeyRerouter.setState(false); hotkeyRerouter.setState(false);
cm.display.wrapper.classList.add("CodeMirror-active"); 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")); var resizeGrip = cm.display.wrapper.appendChild(document.createElement("div"));
resizeGrip.className = "resize-grip"; resizeGrip.className = "resize-grip";
@ -395,6 +420,10 @@ document.addEventListener("wheel", function(event) {
chrome.tabs.query({currentWindow: true}, function(tabs) { chrome.tabs.query({currentWindow: true}, function(tabs) {
var windowId = tabs[0].windowId; var windowId = tabs[0].windowId;
if (prefs.get("openEditInWindow")) { 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) { if (tabs.length == 1 && window.history.length == 1) {
chrome.windows.getAll(function(windows) { chrome.windows.getAll(function(windows) {
if (windows.length > 1) { 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; useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href;
}); });
@ -497,6 +526,23 @@ function addSection(event, section) {
appliesTo.addEventListener("change", onChange); appliesTo.addEventListener("change", onChange);
appliesTo.addEventListener("input", 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"); var sections = document.getElementById("sections");
if (event) { if (event) {
var clickedSection = getSectionForChild(event.target); var clickedSection = getSectionForChild(event.target);
@ -857,36 +903,32 @@ function getEditorInSight(nearbyElement) {
function updateLintReport(cm, delay) { function updateLintReport(cm, delay) {
if (delay == 0) { if (delay == 0) {
// immediately show pending csslint messages in onbeforeunload and save // immediately show pending csslint messages in onbeforeunload and save
update.call(cm); update(cm);
return; return;
} }
if (delay > 0) { if (delay > 0) {
// give csslint some time to find the issues, e.g. 500 (1/10 of our default 5s) setTimeout(cm => { cm.performLint(); update(cm) }, delay, cm);
// by settings its internal delay to 1ms and restoring it back later return;
var lintOpt = editors[0].state.lint.options; }
setTimeout((function(opt, delay) { var state = cm.state.lint;
opt.delay = delay == 1 ? opt.delay : delay; // options object is shared between editors if (!state) {
update(this);
}).bind(cm, lintOpt, lintOpt.delay), delay);
lintOpt.delay = 1;
return; return;
} }
// user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms) // 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 // 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); 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; state.postponeNewIssues = delay == undefined || delay == null;
function update() { // this == cm function update(cm) {
var scope = this ? [this] : editors; var scope = cm ? [cm] : editors;
var changed = false; var changed = false;
var fixedOldIssues = false; var fixedOldIssues = false;
scope.forEach(function(cm) { scope.forEach(function(cm) {
var state = cm.state.lint; var state = cm.state.lint || {};
var oldMarkers = state.markedLast || {}; var oldMarkers = state.markedLast || {};
var newMarkers = {}; var newMarkers = {};
var html = state.marked.length == 0 ? "" : "<tbody>" + var html = !state.marked || state.marked.length == 0 ? "" : "<tbody>" +
state.marked.map(function(mark) { state.marked.map(function(mark) {
var info = mark.__annotation; var info = mark.__annotation;
var isActiveLine = info.from.line == cm.getCursor().line; var isActiveLine = info.from.line == cm.getCursor().line;
@ -938,7 +980,7 @@ function renderLintReport(someBlockChanged) {
var newContent = content.cloneNode(false); var newContent = content.cloneNode(false);
var issueCount = 0; var issueCount = 0;
editors.forEach(function(cm, index) { 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 newBlock = newContent.appendChild(document.createElement("table"));
var html = "<caption>" + label + " " + (index+1) + "</caption>" + cm.state.lint.html; var html = "<caption>" + label + " " + (index+1) + "</caption>" + cm.state.lint.html;
newBlock.innerHTML = html; newBlock.innerHTML = html;
@ -993,7 +1035,7 @@ function beautify(event) {
doBeautify(); doBeautify();
} else { } else {
var script = document.head.appendChild(document.createElement("script")); var script = document.head.appendChild(document.createElement("script"));
script.src = "beautify/beautify-css.js"; script.src = "beautify/beautify-css-mod.js";
script.onload = doBeautify; script.onload = doBeautify;
} }
function doBeautify() { function doBeautify() {
@ -1023,6 +1065,7 @@ function beautify(event) {
if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) { if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
delete cm.beautifyChange[cm.changeGeneration()]; delete cm.beautifyChange[cm.changeGeneration()];
cm.undo(); cm.undo();
cm.scrollIntoView(cm.getCursor());
undoable |= cm.beautifyChange[cm.changeGeneration()]; undoable |= cm.beautifyChange[cm.changeGeneration()];
} }
}); });
@ -1031,6 +1074,9 @@ function beautify(event) {
scope.forEach(function(cm) { scope.forEach(function(cm) {
setTimeout(function() { 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 text = cm.getValue();
var newText = exports.css_beautify(text, options); var newText = exports.css_beautify(text, options);
if (newText != text) { if (newText != text) {
@ -1039,6 +1085,11 @@ function beautify(event) {
cm.beautifyChange = {}; cm.beautifyChange = {};
} }
cm.setValue(newText); 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; cm.beautifyChange[cm.changeGeneration()] = true;
undoButton.disabled = false; undoButton.disabled = false;
} }
@ -1065,48 +1116,67 @@ function beautify(event) {
} }
} }
window.addEventListener("load", init, false); document.addEventListener("DOMContentLoaded", init);
function init() { function init() {
initCodeMirror();
var params = getParams(); var params = getParams();
if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
// This is an add // This is an add
tE("heading", "addStyleTitle");
var section = {code: ""} var section = {code: ""}
for (var i in CssToProperty) { for (var i in CssToProperty) {
if (params[i]) { if (params[i]) {
section[CssToProperty[i]] = [params[i]]; section[CssToProperty[i]] = [params[i]];
} }
} }
addSection(null, section); window.onload = () => {
// default to enabled window.onload = null;
document.getElementById("enabled").checked = true addSection(null, section);
tE("heading", "addStyleTitle"); // default to enabled
initHooks(); document.getElementById("enabled").checked = true
initHooks();
};
return; return;
} }
// This is an edit // This is an edit
tE("heading", "editStyleHeading", null, false); tE("heading", "editStyleHeading", null, false);
requestStyle(); getStylesSafe({id: params.id}).then(styles => {
function requestStyle() { let style = styles[0];
chrome.runtime.sendMessage({method: "getStyles", id: params.id}, function callback(styles) { if (!style) {
if (!styles) { // Chrome is starting up and shows edit.html style = {id: null, sections: []};
requestStyle(); history.replaceState({}, document.title, location.pathname);
return; }
} styleId = style.id;
var style = styles[0]; setStyleMeta(style);
styleId = style.id; window.onload = () => {
initWithStyle(style); 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("name").value = style.name;
document.getElementById("enabled").checked = style.enabled; document.getElementById("enabled").checked = style.enabled;
document.getElementById("url").href = style.url; 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 // if this was done in response to an update, we need to clear existing sections
getSections().forEach(function(div) { div.remove(); }); 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(); var queueStart = new Date().getTime();
// after 100ms the sections will be added asynchronously // after 100ms the sections will be added asynchronously
while (new Date().getTime() - queueStart <= 100 && queue.length) { while (new Date().getTime() - queueStart <= 100 && queue.length) {
@ -1123,7 +1193,11 @@ function initWithStyle(style) {
function add() { function add() {
var sectionDiv = addSection(null, queue.shift()); var sectionDiv = addSection(null, queue.shift());
maximizeCodeHeight(sectionDiv, !queue.length); 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.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(); setupGlobalSearch();
setCleanGlobal(); setCleanGlobal();
updateTitle(); 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) { function maximizeCodeHeight(sectionDiv, isLast) {
var cm = sectionDiv.CodeMirror; var cm = sectionDiv.CodeMirror;
var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []}; var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
@ -1254,14 +1345,14 @@ function save() {
} }
var name = document.getElementById("name").value; var name = document.getElementById("name").value;
var enabled = document.getElementById("enabled").checked; var enabled = document.getElementById("enabled").checked;
var request = { saveStyleSafe({
method: "saveStyle",
id: styleId, id: styleId,
name: name, name: name,
enabled: enabled, enabled: enabled,
reason: 'editSave',
sections: getSectionsHashes() sections: getSectionsHashes()
}; })
chrome.runtime.sendMessage(request, saveComplete); .then(saveComplete);
} }
function getSectionsHashes() { function getSectionsHashes() {
@ -1348,7 +1439,7 @@ function fromMozillaFormat() {
function doImport() { function doImport() {
var replaceOldStyle = this.name == "import-replace"; var replaceOldStyle = this.name == "import-replace";
popup.querySelector(".close-icon").click(); popup.querySelector(".dismiss").onclick();
var mozStyle = trimNewLines(popup.codebox.getValue()); var mozStyle = trimNewLines(popup.codebox.getValue());
var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n"); var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n");
var sectionStack = [{code: "", start: {line: 1, col: 1}}]; var sectionStack = [{code: "", start: {line: 1, col: 1}}];
@ -1423,15 +1514,20 @@ function fromMozillaFormat() {
} }
} }
function doAddSection(section) { 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 (!firstAddedCM) {
if (!initFirstSection(section)) { if (!initFirstSection(section)) {
return; return;
} }
} }
// don't add empty sections
if (!(section.code || section.urls || section.urlPrefixes || section.domains || section.regexps)) {
return;
}
setCleanItem(addSection(null, section), false); setCleanItem(addSection(null, section), false);
firstAddedCM = firstAddedCM || editors.last; firstAddedCM = firstAddedCM || editors.last;
} }
@ -1532,8 +1628,8 @@ function showKeyMapHelp() {
cell.innerHTML = cell.textContent; cell.innerHTML = cell.textContent;
}); });
} }
function mergeKeyMaps(merged) { function mergeKeyMaps(merged, ...more) {
[].slice.call(arguments, 1).forEach(function(keyMap) { more.forEach(keyMap => {
if (typeof keyMap == "string") { if (typeof keyMap == "string") {
keyMap = CodeMirror.keyMap[keyMap]; 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) { function showHelp(title, text) {
var div = document.getElementById("help-popup"); var div = document.getElementById("help-popup");
div.classList.remove("big"); div.classList.remove("big");
@ -1575,14 +1776,16 @@ function showHelp(title, text) {
if (getComputedStyle(div).display == "none") { if (getComputedStyle(div).display == "none") {
document.addEventListener("keydown", closeHelp); 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"; div.style.display = "block";
return div; return div;
function closeHelp(e) { 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 = ""; div.style.display = "";
document.querySelector(".contents").innerHTML = ""; document.querySelector(".contents").innerHTML = "";
document.removeEventListener("keydown", closeHelp); document.removeEventListener("keydown", closeHelp);
@ -1625,11 +1828,21 @@ function getParams() {
return params; return params;
} }
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { chrome.runtime.onMessage.addListener(onRuntimeMessage);
function onRuntimeMessage(request) {
switch (request.method) { switch (request.method) {
case "styleUpdated": case "styleUpdated":
if (styleId && styleId == request.id) { if (styleId && styleId == request.style.id && request.reason != 'editSave') {
initWithStyle(request.style); 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; break;
case "styleDeleted": case "styleDeleted":
@ -1640,14 +1853,93 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
} }
break; break;
case "prefChanged": case "prefChanged":
if (request.prefName == "editor.smartIndent") { if ('editor.smartIndent' in request.prefs) {
CodeMirror.setOption("smartIndent", request.value); CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
} }
break;
case 'editDeleteText':
document.execCommand('delete');
break;
} }
}); }
function getComputedHeight(el) { function getComputedHeight(el) {
var compStyle = getComputedStyle(el); var compStyle = getComputedStyle(el);
return el.getBoundingClientRect().height + return el.getBoundingClientRect().height +
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom); 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) { 'use strict';
if (response.length == 0) {
sendEvent("styleCanBeInstalledChrome"); document.addEventListener('stylishUpdateChrome', onUpdateClicked);
} else { document.addEventListener('stylishInstallChrome', onInstallClicked);
var installedStyle = response[0];
// maybe an update is needed chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// use the md5 if available // orphaned content script check
var md5Url = getMeta("stylish-md5-url"); if (msg.method == 'ping') {
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { sendResponse(true);
getResource(md5Url, function(md5) { }
if (md5 == installedStyle.originalMd5) {
sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl});
} else {
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
}
});
} 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});
}
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});
return;
};
}
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
});
}
}
}); });
function sectionsAreEqual(a, b) { new MutationObserver((mutations, observer) => {
if (a.code != b.code) { if (document.body) {
return false; observer.disconnect();
} chrome.runtime.sendMessage({
return ["urls", "urlPrefixes", "domains", "regexps"].every(function(attribute) { method: 'getStyles',
return arraysAreEqual(a[attribute], b[attribute]); url: getMeta('stylish-id-url') || location.href
}); }, checkUpdatability);
}
}).observe(document.documentElement, {childList: true});
function checkUpdatability([installedStyle]) {
if (!installedStyle) {
sendEvent('styleCanBeInstalledChrome');
return;
}
const md5Url = getMeta('stylish-md5-url');
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
getResource(md5Url).then(md5 => {
reportUpdatable(md5 != installedStyle.originalMd5);
});
} else {
getResource(getMeta('stylish-code-chrome')).then(code => {
reportUpdatable(code === null ||
!styleSectionsEqual(JSON.parse(code), installedStyle));
});
}
function reportUpdatable(isUpdatable) {
sendEvent(
isUpdatable
? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome',
{
updateUrl: installedStyle.updateUrl
}
);
}
} }
function arraysAreEqual(a, b) {
// treat empty array and undefined as equivalent function sendEvent(type, detail = null) {
if (typeof a == "undefined") detail = {detail};
return (typeof b == "undefined") || (b.length == 0); if (typeof cloneInto != 'undefined') {
if (typeof b == "undefined") // Firefox requires explicit cloning, however USO can't process our messages anyway
return (typeof a == "undefined") || (a.length == 0); // because USO tries to use a global "event" variable deprecated in Firefox
if (a.length != b.length) { detail = cloneInto(detail, document); // eslint-disable-line no-undef
return false; }
} onDOMready().then(() => {
return a.every(function(entry) { document.dispatchEvent(new CustomEvent(type, detail));
return b.indexOf(entry) != -1; });
});
} }
function sendEvent(type, data) {
if (typeof data == "undefined") { function onInstallClicked() {
data = null; if (!orphanCheck || !orphanCheck()) {
} return;
var stylishEvent = new CustomEvent(type, {detail: data}); }
document.dispatchEvent(stylishEvent); getResource(getMeta('stylish-description'))
.then(name => saveStyleCode('styleInstall', name))
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
} }
document.addEventListener("stylishUpdateChrome", stylishUpdateChrome);
function stylishInstallChrome() { function onUpdateClicked() {
orphanCheck(); if (!orphanCheck || !orphanCheck()) {
getResource(getMeta("stylish-description"), function(name) { return;
if (confirm(chrome.i18n.getMessage('styleInstall', [name]))) { }
getResource(getMeta("stylish-code-chrome"), function(code) { chrome.runtime.sendMessage({
// check for old style json method: 'getStyles',
var json = JSON.parse(code); url: getMeta('stylish-id-url') || location.href,
json.method = "saveStyle"; }, ([style]) => {
chrome.runtime.sendMessage(json, function(response) { saveStyleCode('styleUpdate', style.name, {id: style.id});
sendEvent("styleInstalledChrome"); });
});
});
getResource(getMeta("stylish-install-ping-url-chrome"));
}
});
} }
document.addEventListener("stylishInstallChrome", stylishInstallChrome);
function stylishUpdateChrome() { function saveStyleCode(message, name, addProps) {
orphanCheck(); return new Promise(resolve => {
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) { if (!confirm(chrome.i18n.getMessage(message, [name]))) {
var style = response[0]; return;
if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) { }
getResource(getMeta("stylish-code-chrome"), function(code) { getResource(getMeta('stylish-code-chrome')).then(code => {
var json = JSON.parse(code); chrome.runtime.sendMessage(
json.method = "saveStyle"; Object.assign(JSON.parse(code), addProps, {
json.id = style.id; method: 'saveStyle',
chrome.runtime.sendMessage(json, function() { reason: 'update',
sendEvent("styleInstalledChrome"); }),
}); () => sendEvent('styleInstalledChrome')
}); );
} resolve();
}); });
});
} }
function getMeta(name) { function getMeta(name) {
var e = document.querySelector("link[rel='" + name + "']"); const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute("href") : null; return e ? e.getAttribute('href') : null;
} }
function getResource(url, callback) {
if (url.indexOf("#") == 0) { function getResource(url) {
if (callback) { return new Promise(resolve => {
callback(document.getElementById(url.substring(1)).innerText); if (url.startsWith('#')) {
} resolve(document.getElementById(url.slice(1)).textContent);
return; } else {
} chrome.runtime.sendMessage({method: 'download', url}, resolve);
var xhr = new XMLHttpRequest(); }
xhr.onreadystatechange = function() { });
if (xhr.readyState == 4 && callback) {
if (xhr.status >= 400) {
callback(null);
} else {
callback(xhr.responseText);
}
}
}
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();
}
} }
/* 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');
});
});
})([
...document.querySelectorAll('div[id^="stylish-installed-style-not-installed-"]'),
...document.querySelectorAll('div[id^="stylish-installed-style-needs-update-"]')
]);
// orphaned content script check function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
}
if (a.length != b.length) {
return false;
}
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 equalOrEmpty(a, b, telltale, comparator) {
const typeA = a && typeof a[telltale] == 'function';
const typeB = b && typeof b[telltale] == 'function';
return (
(a === null || a === undefined || (typeA && !a.length)) &&
(b === null || b === undefined || (typeB && !b.length))
) || typeA && typeB && a.length == b.length && comparator(a, b);
}
function arrayMirrors(array1, array2) {
for (const el of array1) {
if (array2.indexOf(el) < 0) {
return false;
}
}
for (const el of array2) {
if (array1.indexOf(el) < 0) {
return false;
}
}
return true;
}
}
function onDOMready() {
if (document.readyState != 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) =>
msg.method == 'ping' && sendResponse(true));
function orphanCheck() { function orphanCheck() {
var port = chrome.runtime.connect(); const port = chrome.runtime.connect();
if (port) { if (port) {
port.disconnect(); port.disconnect();
return; return true;
} }
// we're orphaned due to an extension update // we're orphaned due to an extension update
// we can detach event listeners // we can detach event listeners
document.removeEventListener('stylishUpdateChrome', stylishUpdateChrome); document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
document.removeEventListener('stylishInstallChrome', stylishInstallChrome); document.removeEventListener('stylishInstallChrome', onInstallClicked);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally // 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 // we can destroy global functions in this context to free up memory
[ [
'arraysAreEqual', 'checkUpdatability',
'getMeta', 'getMeta',
'getResource', 'getResource',
'orphanCheck', 'onDOMready',
'sectionsAreEqual', 'onInstallClicked',
'sendEvent', 'onUpdateClicked',
'stylishUpdateChrome', 'orphanCheck',
'stylishInstallChrome' 'saveStyleCode',
].forEach(fn => window[fn] = null); 'sendEvent',
'styleSectionsEqual',
].forEach(fn => (window[fn] = null));
} }

View File

@ -1,82 +1,121 @@
var template = {}; 'use strict';
const template = {};
tDocLoader(); tDocLoader();
function t(key, params) { function t(key, params) {
var s = chrome.i18n.getMessage(key, params) const cache = !params && t.cache[key];
if (s == "") { const s = cache || chrome.i18n.getMessage(key, params);
throw "Missing string '" + key + "'."; if (s == '') {
} throw `Missing string "${key}"`;
return s; }
} if (!params && !cache) {
function o(key) { t.cache[key] = s;
document.write(t(key)); }
return s;
} }
function tE(id, key, attr, esc) { function tE(id, key, attr, esc) {
if (attr) { if (attr) {
document.getElementById(id).setAttribute(attr, t(key)); 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))); document.getElementById(id).appendChild(document.createTextNode(t(key)));
} else { } else {
document.getElementById(id).innerHTML = t(key); document.getElementById(id).innerHTML = t(key);
} }
} }
function tHTML(html) { 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 node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use &nbsp; for an explicit space
tNodeList(node.querySelectorAll("*")); if (html.includes('i18n-')) {
var child = node.removeChild(node.firstElementChild); tNodeList(node.getElementsByTagName('*'));
node.remove(); }
return child; return node.firstElementChild;
} }
function tNodeList(nodes) { function tNodeList(nodes) {
for (var n = 0; n < nodes.length; n++) { const PREFIX = 'i18n-';
var node = nodes[n]; for (let n = nodes.length; --n >= 0;) {
if (node.nodeType != 1) { // not an ELEMENT_NODE const node = nodes[n];
continue; // skip non-ELEMENT_NODE
} if (node.nodeType != 1) {
if (node.localName == "template") { continue;
tNodeList(node.content.querySelectorAll("*")); }
template[node.dataset.id] = node.content.firstElementChild; if (node.localName == 'template') {
continue; const elements = node.content.querySelectorAll('*');
} tNodeList(elements);
for (var a = node.attributes.length - 1; a >= 0; a--) { template[node.dataset.id] = elements[0];
var attr = node.attributes[a]; // compress inter-tag whitespace to reduce number of DOM nodes by 25%
var name = attr.nodeName; const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
if (name.indexOf("i18n-") != 0) { const toRemove = [];
continue; while (walker.nextNode()) {
} const textNode = walker.currentNode;
name = name.substr(5); // "i18n-".length if (!textNode.nodeValue.trim()) {
var value = t(attr.value); toRemove.push(textNode);
switch (name) { }
case "text": }
node.insertBefore(document.createTextNode(value), node.firstChild); toRemove.forEach(el => el.remove());
break; continue;
case "html": }
node.insertAdjacentHTML("afterbegin", value); for (let a = node.attributes.length; --a >= 0;) {
break; const attr = node.attributes[a];
default: const name = attr.nodeName;
node.setAttribute(name, value); if (!name.startsWith(PREFIX)) {
} continue;
node.removeAttribute(attr.nodeName); }
} const type = name.substr(PREFIX.length);
} const value = t(attr.value);
switch (type) {
case 'text':
node.insertBefore(document.createTextNode(value), node.firstChild);
break;
case 'text-append':
node.appendChild(document.createTextNode(value));
break;
case 'html':
node.insertAdjacentHTML('afterbegin', value);
break;
default:
node.setAttribute(type, value);
}
node.removeAttribute(name);
}
}
} }
function tDocLoader() { function tDocLoader() {
// localize HEAD t.cache = tryJSONparse(localStorage.L10N) || {};
tNodeList(document.querySelectorAll("*")); const cacheLength = Object.keys(t.cache).length;
// localize HEAD
tNodeList(document.getElementsByTagName('*'));
// localize BODY // localize BODY
var observer = new MutationObserver(function(mutations) { const process = mutations => {
for (var m = 0; m < mutations.length; m++) { for (const mutation of mutations) {
tNodeList(mutations[m].addedNodes); tNodeList(mutation.addedNodes);
} }
}); };
observer.observe(document, {subtree: true, childList: true}); const observer = new MutationObserver(process);
document.addEventListener("DOMContentLoaded", function() { const onLoad = () => {
observer.disconnect(); tDocLoader.stop();
tNodeList(document.querySelectorAll("*")); 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});
};
tDocLoader.stop = () => {
observer.disconnect();
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,300 +1,223 @@
<html> <html id="stylus">
<head> <head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title i18n-text="manageTitle"></title> <title i18n-text="manageTitle"></title>
<style> <link rel="stylesheet" href="manage.css">
body { <link rel="stylesheet" href="msgbox/msgbox.css">
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 */
.update,
.check-update {
display: none;
}
/* Check update button for things that can*/
*[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>
<template data-id="style"> <style id="style-overrides"></style>
<div>
<h2 class="style-name"></h2>
<p class="applies-to"></p>
<p class="actions">
<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>
<button class="check-update" i18n-text="checkForUpdate"></button>
<button class="update" i18n-text="installUpdate"></button>
<span class="update-note"></span>
</p>
</div>
</template>
<template data-id="styleHomepage"> <!-- Notes:
<a target="_blank"> * Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" class="svg-icon installed" height="16" width="16" viewBox="0 0 8 8"> * inter-tag whitespace in templates is automatically removed in localization.js
<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> * i18n-anything attribute automatically creates "anything" attribute
</svg> -->
<template data-id="style">
<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>
</template> <button class="enable" i18n-text="enableStyleLabel"></button>
<button class="disable" i18n-text="disableStyleLabel"></button>
<button class="delete" i18n-text="deleteStyleLabel"></button>
<button class="check-update" i18n-text="checkForUpdate"></button>
<button class="update" i18n-text="installUpdate"></button>
<span class="update-note"></span>
</p>
</div>
</template>
<script src="localization.js"></script> <template data-id="styleCompact">
<script src="health.js"></script> <div class="entry">
<script src="storage.js"></script> <h2 class="style-name">
<script src="messaging.js"></script> <input class="checker" type="checkbox" i18n-title="toggleStyle">
<script src="apply.js"></script> <a class="style-name-link" href="edit.html?id="></a>
<script src="manage.js"></script> </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>
</span>
</p>
<div class="applies-to">
<div class="targets"></div>
<span class="expander">...</span>
</div>
</div>
</template>
<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> </head>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage"> <body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
<div id="header"> <div id="header">
<h1 id="manage-heading" i18n-text="manageHeading"></h1> <h1 id="manage-heading" i18n-text="manageHeading"></h1>
<fieldset> <fieldset>
<legend id="filters" i18n-text="manageFilters"></legend> <legend id="filters" i18n-text="manageFilters"></legend>
<div> <label>
<input id="manage.onlyEnabled" type="checkbox"> <input id="manage.onlyEnabled" type="checkbox"
<label id="manage.onlyEnabled-label" for="manage.onlyEnabled" i18n-text="manageOnlyEnabled"></label> data-filter=".enabled"
</div> data-filter-hide=".disabled">
<div> <span i18n-text="manageOnlyEnabled"></span>
<input id="manage.onlyEdited" type="checkbox"> </label>
<label id="manage.onlyEdited-label" for="manage.onlyEdited" i18n-text="manageOnlyEdited"></label> <label>
</div> <input id="manage.onlyLocal" type="checkbox"
<div> data-filter=":not(.updatable)"
<input id="search" type="search" i18n-placeholder="searchStyles"> data-filter-hide=".updatable">
</div> <span i18n-text="manageOnlyLocal" i18n-title="manageOnlyLocalTooltip"></span>
</fieldset> </label>
<p> <label id="onlyUpdates" class="hidden">
<button id="check-all-updates" i18n-text="checkAllUpdates"></button> <input type="checkbox"
</p> data-filter=".can-update, .update-problem, .update-done"
<p> data-filter-hide=":not(.updatable):not(.update-done), .no-update:not(.update-problem)">
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button> <span i18n-text="manageOnlyUpdates"></span>
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span> </label>
</p> <input id="search" type="search" i18n-placeholder="searchStyles"
<p> data-filter=":not(.not-matching)"
<a href="edit.html"> data-filter-hide=".not-matching">
<button id="add-style-label" i18n-text="addStyleLabel"></button> </fieldset>
</a> <p>
</p> <button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
<div id="options"> <span id="update-history" i18n-title="genericHistoryLabel">
<h2 id="options-heading" i18n-text="optionsHeading"></h2> <svg class="svg-icon" viewBox="0 0 20 20" i18n-alt="helpAlt">
<div> <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"/>
<input id="show-badge" type="checkbox"> </svg>
<label id="show-badge-label" for="show-badge" i18n-text="prefShowBadge"></label> </span>
</div> </p>
<div> <p>
<input id="popup.stylesFirst" type="checkbox"> <button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
<label id="stylesFirst-label" for="popup.stylesFirst" i18n-text="popupStylesFirst"></label> <span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
</div> <button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
<div id="more-options"> </p>
<h3 id="options-subheading" i18n-text="optionsSubheading"></h3> <p>
<button id="manage-options-button" i18n-text="openOptionsManage"></button> <a href="edit.html">
<button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button> <button id="add-style-label" i18n-text="addStyleLabel"></button>
<p> </a>
<button id="editor-styles-button" i18n-text="editorStylesButton"></button> </p>
</p> <div id="options">
</div> <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="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="manage.newUI.faviconsGray" type="checkbox">
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray"></label>
</div>
</div> </div>
<div id="backup"> </div>
<h2 id="backup-title" i18n-text="backupButtons"></h2> <label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
<span id="backup-message" i18n-text="backupMessage"></span>
<p>
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
</p>
</div>
<p id="manage-text" i18n-html="manageText"></p>
</div> </div>
<div id="installed"></div> <p>
<script src="openOptions.js"></script> <button id="manage-options-button" i18n-text="openOptionsManage"></button>
<script src="backup/fileSaveLoad.js"></script> <button id="manage-shortcuts-button" class="chromium-only"
</body> 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 id="backup">
<h2 id="backup-title" i18n-text="backupButtons"></h2>
<span id="backup-message" i18n-text="backupMessage"></span>
<p>
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
</p>
</div>
<p id="manage-text" i18n-html="manageText"></p>
</div>
<div id="installed"></div>
<script src="backup/fileSaveLoad.js"></script>
<script src="msgbox/msgbox.js"></script>
</body>
</html> </html>

1325
manage.js

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,25 @@
{ {
"name": "Stylus", "name": "Stylus",
"version": "1.0.5", "version": "1.0.5",
"minimum_chrome_version": "49",
"description": "__MSG_description__", "description": "__MSG_description__",
"homepage_url": "http://add0n.com/stylus.html", "homepage_url": "http://add0n.com/stylus.html",
"manifest_version": 2, "manifest_version": 2,
"icons": { "icons": {
"16": "16.png", "16": "/images/icon/16.png",
"48": "48.png", "32": "/images/icon/32.png",
"128": "128.png" "48": "/images/icon/48.png",
"128": "/images/icon/128.png"
}, },
"permissions": [ "permissions": [
"tabs", "tabs",
"webNavigation", "webNavigation",
"contextMenus", "contextMenus",
"storage", "storage",
"*://*/*" "<all_urls>"
], ],
"background": { "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": { "commands": {
"openManage": { "openManage": {
@ -32,21 +34,22 @@
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"run_at": "document_start", "run_at": "document_start",
"all_frames": true, "all_frames": true,
"match_about_blank": true,
"js": ["apply.js"] "js": ["apply.js"]
}, },
{ {
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"], "matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
"run_at": "document_end", "run_at": "document_start",
"all_frames": false, "all_frames": false,
"js": ["install.js"] "js": ["install.js"]
} }
], ],
"browser_action": { "browser_action": {
"default_icon": { "default_icon": {
"16": "16w.png", "16": "/images/icon/16w.png",
"32": "32w.png", "32": "/images/icon/32w.png",
"19": "19w.png", "19": "/images/icon/19w.png",
"38": "38w.png" "38": "/images/icon/38w.png"
}, },
"default_title": "Stylus", "default_title": "Stylus",
"default_popup": "popup.html" "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 // keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true; const KEEP_CHANNEL_OPEN = true;
const OWN_ORIGIN = chrome.runtime.getURL('');
function notifyAllTabs(request) { const FIREFOX = /Firefox/.test(navigator.userAgent);
// list all tabs including chrome-extension:// which can be ours const OPERA = /OPR/.test(navigator.userAgent);
chrome.tabs.query({}, tabs => {
for (let tab of tabs) { const URLS = {
if (request.codeIsUpdated !== false || tab.url.startsWith(OWN_ORIGIN)) { ownOrigin: chrome.runtime.getURL(''),
chrome.tabs.sendMessage(tab.id, request);
updateIcon(tab); optionsUI: [
} chrome.runtime.getURL('options/index.html'),
} 'chrome://extensions/?options=' + chrome.runtime.id,
}); ],
// notify all open popups
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method}); configureCommands:
chrome.runtime.sendMessage(reqPopup); OPERA ? 'opera://settings/configureCommands'
// notify self: the message no longer is sent to the origin in new Chrome : 'chrome://extensions/configureCommands',
if (typeof applyOnMessage !== 'undefined') {
applyOnMessage(reqPopup); // 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 refreshAllTabs() { function notifyAllTabs(msg) {
return new Promise(resolve => { const originalMessage = msg;
// list all tabs including chrome-extension:// which can be ours if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
chrome.tabs.query({}, tabs => { // apply/popup/manage use only meta for these two methods,
const lastTab = tabs[tabs.length - 1]; // editor may need the full code but can fetch it directly,
for (let tab of tabs) { // so we send just the meta to avoid spamming lots of tabs with huge styles
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { msg = Object.assign({}, msg, {
const message = {method: 'styleReplaceAll', styles}; style: getStyleWithNoCode(msg.style)
if (tab.url == location.href && typeof applyOnMessage !== 'undefined') { });
applyOnMessage(message); }
} else { const affectsAll = !msg.affects || msg.affects.all;
chrome.tabs.sendMessage(tab.id, message); const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
} const affectsTabs = affectsAll || affectsOwnOriginOnly;
updateIcon(tab, styles); const affectsIcon = affectsAll || msg.affects.icon;
if (tab == lastTab) { const affectsPopup = affectsAll || msg.affects.popup;
resolve(); const affectsSelf = affectsPopup || msg.prefs;
} if (affectsTabs || affectsIcon) {
}); // list all tabs including chrome-extension:// which can be ours
} 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 self: the message no longer is sent to the origin in new Chrome
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 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") {
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);
} else {
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url, enabled: true}, stylesReceived);
}
});
function stylesReceived(styles) { function getTab(id) {
var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll"); return new Promise(resolve =>
var postfix = disableAll ? "x" : styles.length == 0 ? "w" : ""; chrome.tabs.get(id, tab =>
chrome.browserAction.setIcon({ !chrome.runtime.lastError && resolve(tab)));
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) { function getActiveTab() {
callback(tabs[0]); return new Promise(resolve =>
}); chrome.tabs.query({currentWindow: true, active: true}, tabs =>
resolve(tabs[0])));
} }
function getActiveTabRealURL(callback) {
getActiveTab(function(tab) { function getActiveTabRealURL() {
getTabRealURL(tab, callback); return getActiveTab()
}); .then(getTabRealURL);
} }
function getTabRealURL(tab, callback) {
if (tab.url != "chrome://newtab/") { function getTabRealURL(tab) {
callback(tab.url); return new Promise(resolve => {
} else { if (tab.url != 'chrome://newtab/') {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, function(frame) { resolve(tab.url);
frame && callback(frame.url); } else {
}); chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
} resolve(frame && frame.url || '');
});
}
});
} }
// 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;
}
}
getActiveTab().then(tab => {
if (tab && tab.url == 'chrome://newtab/') {
chrome.tabs.update({url}, resolve);
} else {
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
}
});
});
});
}
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 stringAsRegExp(s, flags) { function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags); return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
} }
// expands * as .*?
function wildcardAsRegExp(s, flags) { function ignoreChromeError() {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, "\\$&").replace(/\*/g, '.*?'), flags); chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
} }
var configureCommands = {
get url () { function getStyleWithNoCode(style) {
return navigator.userAgent.indexOf('OPR') > -1 ? const stripped = Object.assign({}, style, {sections: []});
'opera://settings/configureCommands' : for (const section of style.sections) {
'chrome://extensions/configureCommands' stripped.sections.push(Object.assign({}, section, {code: null}));
}, }
open: () => { return stripped;
chrome.tabs.create({ }
'url': configureCommands.url
});
} // 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);
},
});
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 { html.opera {
margin: 10px; text-align: center;
font-family: "Helvetica Neue",Helvetica,sans-serif;
font-size: 12px;
} }
table { html.opera body {
width: 100%; 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; text-align: right;
} }
input[type=number], input[type=number]:invalid {
button { background-color: rgba(255, 0, 0, 0.1);
width: 80px; 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%; font-size: 90%;
color: #999; 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> <!DOCTYPE html>
<html> <html id="stylus">
<head> <head>
<title>Stylus Options</title> <title i18n-text-append="optionsHeading">Stylus </title>
<link rel="stylesheet" href="index.css"> <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> </head>
<body> <body>
<h1 i18n-text="optionsCustomize"></h1> <div id="options">
<table>
<tbody> <div class="block">
<tr> <h1 i18n-text="optionsCustomizeBadge"></h1>
<td i18n-text="optionsBadgeNormal"></td> <div class="items">
<td><input type="color" id="badgeNormal"></td> <label>
</tr> <span i18n-text="prefShowBadge"></span>
<tr> <span class="onoffswitch">
<td i18n-text="optionsBadgeDisabled"></td> <input type="checkbox" id="show-badge">
<td><input type="color" id="badgeDisabled"></td> <span></span>
</tr> </span>
<tr> </label>
<td i18n-text="optionsPopupWidth"></td> <label>
<td><input type="number" id="popupWidth" min="200"></td> <span i18n-text="optionsBadgeNormal"></span>
</tr> <input type="color" id="badgeNormal">
<tr> </label>
<td i18n-text="optionsUpdateInterval"><sup>1</sup></td> <label>
<td><input type="number" min="0" id="updateInterval"></td> <span i18n-text="optionsBadgeDisabled"></span>
</tr> <input type="color" id="badgeDisabled">
</tbody> </label>
</table> </div>
<div> </div>
<button id="save">Save</button>
<span id="status"></span> <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>
<h1 i18n-text="optionsActions"></h1> <div id="notes">
<table> <ol>
<tbody> <li>
<tr> <p i18n-text="optionsUpdateIntervalNote"></p>
<td i18n-text="optionsOpenManager"><sup>2</sup></td> <p i18n-text="optionsUpdateImportNote"></p>
<td><button type="button" data-cmd="open-manage" i18n-text="optionsOpen"></button></td> </li>
</tr> <li i18n-text="optionsAdvancedExposeIframesNote"></li>
<tr> </ol>
<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> </div>
<script src="/messaging.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
</body> </body>
</html> </html>

View File

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

406
popup.css
View File

@ -1,30 +1,53 @@
body { body {
width: 252px; width: 252px;
font-size: 12px; font-size: 12px;
font-family: Arial,"Helvetica Neue",Helvetica,sans-serif; 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] { input[type=checkbox] {
outline: none; outline: none;
} }
#disable-all-wrapper { #disable-all-wrapper {
padding: 0.3em 0 0.6em; padding: 0.3em 0 0.6em;
} }
#no-styles { #no-styles {
font-style: italic; font-style: italic;
} }
#popup-shortcuts-button { #popup-shortcuts-button {
margin-left: 3px; margin-left: 3px;
} }
.checker { .checker {
display: inline; display: inline;
} }
.style-name { .style-name {
cursor: default; cursor: default;
font-weight: bold; font-weight: bold;
display: block; display: block;
} }
a, a:visited {
color: black; a {
color: #000;
transition: color .5s;
text-decoration-skip: ink;
}
a:hover {
color: #666;
} }
.left-gutter { .left-gutter {
@ -32,137 +55,289 @@ a, a:visited {
width: 16px; width: 16px;
vertical-align: top; vertical-align: top;
} }
.left-gutter input { .left-gutter input {
margin-bottom: 1px; margin-bottom: 1px;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
} }
.main-controls { .main-controls {
display: table-cell; display: table-cell;
} }
.entry {
padding: 0.5em 0;
}
.entry:first-child {
padding-top: 0;
}
#unavailable,
#installed { #installed {
border-bottom: 1px solid black; border-bottom: 1px solid black;
padding-bottom: 2px; padding-bottom: 2px;
} }
body > DIV:last-of-type, body > DIV:last-of-type,
body.blocked > DIV { body.blocked > DIV {
border-bottom: none; border-bottom: none;
} }
#installed { #installed {
padding-top: 2px; padding-top: 2px;
max-height: 434px; max-height: 434px;
overflow-y: auto; overflow-y: auto;
} }
#installed.disabled .style-name { #installed.disabled .style-name {
text-decoration: line-through; text-decoration: line-through;
} }
#installed .actions { #installed .actions {
cursor: default; cursor: default;
} }
#installed .actions a { #installed .actions a {
cursor: pointer; cursor: pointer;
text-decoration: none; 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; margin-right: 2px;
} }
#installed .style-edit-link, #installed .delete {
.entry .style-edit-link,
.entry .delete {
display: inline-block; display: inline-block;
padding: 0 1px 0; 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 { .svg-icon {
pointer-events: none; pointer-events: none;
transition: fill .5s; transition: fill .5s;
width: 14px;
height: 16px;
fill: #666;
} }
a:hover .svg-icon { a:hover .svg-icon {
fill: #000000; fill: #000;
} }
body > .actions { body > .actions {
margin-top: 0.5em; 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; margin-bottom: 0.75em;
} }
.actions input, .actions label {
.actions input,
.actions label {
vertical-align: middle; vertical-align: middle;
} }
#unavailable { body.blocked #installed > *,
border: none; body.blocked .actions > .main-controls,
display: none; /* flex */ body.blocked .actions > .left-gutter {
align-items: center;
justify-content: center;
font-size: 14px;
}
body.blocked #installed,
body.blocked #find-styles,
body.blocked #write-style,
body:not(.blocked) #unavailable {
display: none; display: none;
} }
/* Never shown, but can be enabled with a style */ /* Never shown, but can be enabled with a style */
.enable, .disable {
.enable,
.disable {
display: none; display: none;
} }
/* 'New style' links */ /* 'New style' links */
#write-style-for {margin-right: .6ex}
.write-style-link {margin-left: .6ex} #write-style {
.write-style-link::before, .write-style-link::after {font-size: x-small} display: flex;
.write-style-link::before {content: "\00ad"} /* "soft" hyphen */ flex-direction: row;
#match {overflow-wrap: break-word;} 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" 'new style' links */
.breadcrumbs > .write-style-link {margin-left: 0} .breadcrumbs > .write-style-link {
.breadcrumbs:hover a {color: #bbb; text-decoration: none} 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
}
/* 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))::before { .breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2))::before {
content: attr(subdomain); content: attr(subdomain);
} }
/* "dot" after each subdomain name */ /* "dot" after each subdomain name */
.breadcrumbs > .write-style-link[subdomain]::after {content: "."} .breadcrumbs > .write-style-link[subdomain]::after {
/* no "dot" after top-level domain */ content: "."
.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/"} /* no "dot" after top-level domain */
.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:first-child::before, .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 */ /* suppress TLD-only link */
.breadcrumbs > .write-style-link[subdomain=""] {display: none} .breadcrumbs > .write-style-link[subdomain=""] {
display: none
}
/* :hover style */ /* :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:hover,
.breadcrumbs > .write-style-link:focus, .breadcrumbs > .write-style-link:focus,
.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain], .breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain],
.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] { .breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] {
color: inherit; color: inherit;
text-decoration: underline; text-decoration: underline;
text-decoration-skip: ink;
} }
/* action buttons */ /* action buttons */
#popup-options { #popup-options {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
padding: 1.2em 0; padding: 1.2em 0;
} }
#popup-options button { #popup-options button {
margin: 0 2px; margin: 0 2px;
width: 33%; width: 33%;
@ -171,83 +346,126 @@ body:not(.blocked) #unavailable {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* margins */ /* confirm */
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,
#confirm>div>span { #confirm > div > span {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#confirm { #confirm {
z-index: 2147483647; z-index: 2147483647;
display: none; /* flex */ display: none;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0!important; margin: 0 !important;
box-sizing: border-box; box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.4); 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] { #confirm[data-display=true] {
display: flex; display: flex;
} }
#confirm>div {
#confirm > div {
width: 80%; width: 80%;
height: 100px; height: 100px;
max-height: 80%; max-height: 80%;
min-height: 8em;
background-color: #fff; background-color: #fff;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: solid 2px rgba(0, 0, 0, 0.5); border: solid 2px rgba(0, 0, 0, 0.5);
} }
#confirm>div>span {
#confirm > div > span {
display: flex; display: flex;
flex: 1; flex: 1;
padding: 0 10px; padding: 0 10px;
} }
#confirm>div>b {
#confirm > div > b {
padding: 10px; padding: 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#confirm>div>div {
#confirm > div > div {
padding: 10px; 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,85 +1,109 @@
<html> <html id="stylus">
<head> <head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css">
<template data-id="style"> <!-- Notes:
<div> * Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
<div class="left-gutter"> * inter-tag whitespace in templates is automatically removed in localization.js
<input class="checker" type="checkbox"> * i18n-anything attribute automatically creates "anything" attribute
</div> -->
<div class="main-controls">
<label class="style-name"></label> <template data-id="style">
<div class="actions"> <div class="entry">
<a href="#" class="enable" i18n-text="enableStyleLabel"></a> <div class="left-gutter">
<a href="#" class="disable" i18n-text="disableStyleLabel"></a> <input class="checker" type="checkbox">
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel"> <!--`i18n-title` automatically creates `title` attribute --> </div>
<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"> <div class="main-controls">
<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> <label class="style-name"></label>
</svg> <div class="actions">
</a> <a href="#" class="enable" i18n-text="enableStyleLabel"></a>
<a href="#" class="delete" i18n-title="deleteStyleLabel"> <a href="#" class="disable" i18n-text="disableStyleLabel"></a>
<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"> <a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel">
<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 edit" viewBox="0 0 14 16">
</svg> <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"/>
</a> </svg>
</div> </a>
</div> <a href="#" class="delete" i18n-title="deleteStyleLabel">
<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> </div>
</template> </div>
</div>
</template>
<script src="localization.js"></script> <template data-id="writeStyle">
<script src="health.js"></script> <a class="write-style-link"></a>
<script src="storage.js"></script> </template>
<script src="messaging.js"></script>
<script src="apply.js"></script> <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> </head>
<body id="stylus-popup"> <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">
</div>
</div>
</div>
<div id="unavailable"> <div id="confirm">
<div class="main-controls"><span id="unavailable-message" i18n-text="stylishUnavailableForURL"></span> <div>
</div> <b>Style's Name</b>
<span i18n-text="deleteStyleConfirm"></span>
<div>
<button i18n-text="confirmDelete" data-cmd="ok"></button>
<button i18n-text="confirmCancel" data-cmd="cancel"></button>
</div>
</div>
</div>
<div id="installed"></div>
<div class="actions">
<div id="disable-all-wrapper">
<div class="left-gutter">
<input id="disableAll" type="checkbox">
</div>
<div class="main-controls">
<label id="disableAll-label" for="disableAll" i18n-text="disableAllStyles"></label>
</div>
</div>
<div class="left-gutter"></div>
<div class="main-controls">
<div id="find-styles">
<a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a>
</div>
<div id="write-style">
<span id="write-style-for" i18n-text="writeStyleFor"></span>
</div>
</div> </div>
<div id="installed"></div>
<div class="actions">
<div id="disable-all-wrapper">
<div class="left-gutter">
<input id="disableAll" type="checkbox">
</div>
<div class="main-controls">
<label id="disableAll-label" for="disableAll" i18n-text="disableAllStyles"></label>
</div>
</div>
<div class="left-gutter"></div>
<div class="main-controls">
<div id="find-styles">
<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>
</div>
</div>
<!-- Actions --> <!-- Actions -->
<div id="popup-options"> <div id="popup-options">
<button id="popup-manage-button" i18n-text="openManage"></button> <button id="popup-manage-button" i18n-text="openManage" data-href="manage.html"></button>
<button id="popup-options-button" i18n-text="openOptionsPopup"> <button id="popup-options-button" i18n-text="openOptionsPopup"></button>
<button id="popup-shortcuts-button" i18n-text="openShortcutsPopup"></button> <button id="popup-shortcuts-button" class="chromium-only"
i18n-text="shortcuts"
i18n-title="shortcutsNote"></button>
</div> </div>
</div> </div>
<script src="popup.js"></script>
</body> </body>
</html> </html>

593
popup.js
View File

@ -1,246 +1,407 @@
/* globals configureCommands */ 'use strict';
var writeStyleTemplate = document.createElement("a"); let installed;
writeStyleTemplate.className = "write-style-link"; let tabURL;
const handleEvent = {};
var installed = document.getElementById("installed"); getActiveTabRealURL().then(url => {
tabURL = URLS.supported.test(url) ? url : '';
Promise.all([
tabURL && getStylesSafe({matchUrl: tabURL}),
onDOMready().then(() => {
initPopup(tabURL);
}),
]).then(([styles]) => {
showStyles(styles);
});
});
if (!prefs.get("popup.stylesFirst")) { chrome.runtime.onMessage.addListener(onRuntimeMessage);
document.body.insertBefore(document.querySelector("body > .actions"), installed);
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;
}
} }
getActiveTabRealURL(updatePopUp);
function updatePopUp(url) { function setPopupWidth(width = prefs.get('popupWidth')) {
var urlWillWork = /^(file|http|https|ftps?|chrome\-extension):/.exec(url); document.body.style.width =
if (!urlWillWork) { Math.max(200, Math.min(800, width)) + 'px';
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);
});
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));
}
writeStyle.appendChild(container);
} }
function initPopup(url) {
installed = $('#installed');
setPopupWidth();
// force Chrome to resize the popup
if (!FIREFOX) {
document.body.style.height = '10px';
document.documentElement.style.height = '10px';
}
// action buttons
$('#disableAll').onchange = function() {
installed.classList.toggle('disabled', this.checked);
};
setupLivePrefs();
$('#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) { function showStyles(styles) {
var enabledFirst = prefs.get("popup.enabledFirst"); if (!styles) {
styles.sort(function(a, b) { return;
if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1; }
return a.name.localeCompare(b.name); if (!styles.length) {
}); installed.innerHTML = template.noStyles.outerHTML;
if (styles.length == 0) { return;
installed.innerHTML = "<div class='entry' id='no-styles'>" + t('noStylesForSite') + "</div>"; }
}
styles.map(createStyleElement).forEach(function(e) { const enabledFirst = prefs.get('popup.enabledFirst');
installed.appendChild(e); styles.sort((a, b) => (
}); enabledFirst && a.enabled !== b.enabled
// force Chrome to resize the popup ? !(a.enabled < b.enabled) ? -1 : 1
document.body.style.height = '10px'; : a.name.localeCompare(b.name)
document.documentElement.style.height = '10px'; ));
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) {
var e = template.style.cloneNode(true);
var checkbox = e.querySelector(".checker");
checkbox.id = "style-" + style.id;
checkbox.checked = style.enabled;
e.setAttribute("class", "entry " + (style.enabled ? "enabled" : "disabled")); function createStyleElement({
e.setAttribute("style-id", style.id); style,
var styleName = e.querySelector(".style-name"); container = installed,
styleName.appendChild(document.createTextNode(style.name)); postponeDetect,
styleName.setAttribute("for", "style-" + style.id); }) {
styleName.checkbox = checkbox; const entry = template.style.cloneNode(true);
var editLink = e.querySelector(".style-edit-link"); entry.setAttribute('style-id', style.id);
editLink.setAttribute("href", editLink.getAttribute("href") + style.id); Object.assign(entry, {
editLink.addEventListener("click", openLinkInTabOrWindow, false); id: 'style-' + style.id,
styleId: style.id,
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
onmousedown: handleEvent.maybeEdit,
});
styleName.addEventListener("click", function() { this.checkbox.click(); event.preventDefault(); }); const checkbox = $('.checker', entry);
// clicking the checkbox will toggle it, and this will run after that happens Object.assign(checkbox, {
checkbox.addEventListener("click", function() { enable(event, event.target.checked); }, false); id: 'style-' + style.id,
e.querySelector(".enable").addEventListener("click", function() { enable(event, true); }, false); checked: style.enabled,
e.querySelector(".disable").addEventListener("click", function() { enable(event, false); }, false); onclick: handleEvent.toggle,
});
e.querySelector(".delete").addEventListener("click", function() { doDelete(event, false); }, false); const editLink = $('.style-edit-link', entry);
return e; 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;
styleName.appendChild(document.createTextNode(style.name));
$('.enable', entry).onclick = handleEvent.toggle;
$('.disable', entry).onclick = handleEvent.toggle;
$('.delete', entry).onclick = handleEvent.delete;
if (postponeDetect) {
setTimeout(detectSloppyRegexps, 0, {entry, style});
} else {
detectSloppyRegexps({entry, style});
}
const oldElement = $('#style-' + style.id);
if (oldElement) {
oldElement.parentNode.replaceChild(entry, oldElement);
} else {
container.appendChild(entry);
}
} }
function enable(event, enabled) {
var id = getId(event);
enableStyle(id, enabled);
}
function doDelete() { Object.assign(handleEvent, {
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;
} getClickedStyleId(event) {
document.getElementById('confirm').addEventListener('click', e => { return (handleEvent.getClickedStyleElement(event) || {}).styleId;
let cmd = e.target.dataset.cmd; },
if (cmd === 'ok') {
deleteStyle(document.getElementById('confirm').dataset.id, () => { getClickedStyleElement(event) {
// update view with 'No styles installed for this site' message return event.target.closest('.entry');
if (document.getElementById('installed').children.length === 0) { },
showStyles([]);
} name(event) {
}); this.checkbox.click();
} event.preventDefault();
// },
if (cmd) {
document.getElementById('confirm').dataset.display = false; 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);
}
};
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 (!installed.children.length) {
showStyles([]);
}
});
}
}
},
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) { function handleUpdate(style) {
var styleElement = installed.querySelector("[style-id='" + style.id + "']"); if ($('#style-' + style.id)) {
if (styleElement) { createStyleElement({style});
installed.replaceChild(createStyleElement(style), styleElement); return;
} else { }
getActiveTabRealURL(function(url) { // Add an entry when a new style for the current url is installed
if (chrome.extension.getBackgroundPage().getApplicableSections(style, url).length) { if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
// a new style for the current url is installed document.body.classList.remove('blocked');
document.getElementById("unavailable").style.display = "none"; createStyleElement({style});
installed.appendChild(createStyleElement(style)); }
}
});
}
} }
function handleDelete(id) { function handleDelete(id) {
var styleElement = installed.querySelector("[style-id='" + id + "']"); $$('#style-' + id).forEach(el => el.remove());
if (styleElement) {
installed.removeChild(styleElement);
}
} }
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;
}
}
});
["find-styles-link"].forEach(function(id) { /*
document.getElementById(id).addEventListener("click", openLink, false); 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;
document.getElementById("disableAll").addEventListener("change", function(event) { BG.compileStyleRegExps({style, compileAll: true});
installed.classList.toggle("disabled", prefs.get("disableAll")); entry.hasInvalidRegexps = wannabeSections.some(section =>
}); section.regexps.some(rx => !BG.cachedStyles.regexps.has(rx)));
setupLivePrefs(["disableAll"]); entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
document.querySelector('#popup-manage-button').addEventListener("click", function() { if (!appliedSections.length) {
window.open(chrome.runtime.getURL('manage.html')); entry.classList.add('not-applied');
}); $('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
}
document.querySelector('#popup-options-button').addEventListener("click", function() { if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
if (chrome.runtime.openOptionsPage) { entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
// Supported (Chrome 42+) entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
chrome.runtime.openOptionsPage(); const indicator = template.regexpProblemIndicator.cloneNode(true);
} else { indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
// Fallback indicator.onclick = handleEvent.indicator;
window.open(chrome.runtime.getURL('options/index.html')); $('.main-controls', entry).appendChild(indicator);
} }
}); }
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); });
}
}

1138
storage.js

File diff suppressed because it is too large Load Diff

259
update.js
View File

@ -1,115 +1,162 @@
/* globals getStyles, saveStyle, prefs */ /* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
/* global getStyleDigests, updateStyleDigest */
'use strict'; 'use strict';
var update = { // eslint-disable-next-line no-var
fetch: (resource, callback) => { var updater = {
let req = new XMLHttpRequest();
let [url, data] = resource.split('?'); COUNT: 'count',
req.open('POST', url, true); UPDATED: 'updated',
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); SKIPPED: 'skipped',
req.onload = () => callback(req.responseText); DONE: 'done',
req.onerror = req.ontimeout = () => callback();
req.send(data); // 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;
});
}, },
md5Check: (style, callback, skipped) => {
let req = new XMLHttpRequest(); checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
req.open('GET', style.md5Url, true); let hasDigest;
req.onload = () => { /*
let md5 = req.responseText; Original style digests are calculated in these cases:
if (md5 && md5 !== style.originalMd5) { * style is installed or updated from server
callback(style); * 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);
} }
else { return download(style.md5Url);
skipped(`"${style.name}" style is up-to-date`); }
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.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',
}));
}
function styleJSONseemsValid(json) {
return json
&& json.sections
&& json.sections.length
&& typeof json.sections.every == 'function'
&& typeof json.sections[0].code == 'string';
}
},
schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval) {
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);
}; };
req.onerror = req.ontimeout = () => skipped('Error validating MD5 checksum'); function flushQueue() {
req.send(); chromeLocal.getValue('updateLog').then((lines = []) => {
}, const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
list: (callback) => { if (!queue[0].text) {
getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl))); queue.shift();
}, if (lines[lines.length - 1]) {
perform: (observe = function () {}) => { lines.push('');
// 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;
});
}
// 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]);
});
}
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
}
json.method = 'saveStyle';
json.id = style.id;
saveStyle(json).then(style => {
observe('single-updated', style.name);
});
}
else {
return observe('single-skipped', '3'); // style sections mismatch
} }
} }
}), () => observe('single-skipped', '1'))); 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 = [];
});
}
})(),
}; };
// automatically update all user-styles if "updateInterval" pref is set
window.setTimeout(function () { updater.schedule();
let id; prefs.subscribe(updater.schedule, ['updateInterval']);
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
if (interval) {
/* console.log('next update', interval); */
id = window.setTimeout(run, interval * 60 * 60 * 1000);
}
}
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();
}
});
}, 10000);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB