Merge pull request #134 from eight04/dev-user-css
Install styles from *.user.css file
This commit is contained in:
commit
1d463d7820
|
@ -7,6 +7,10 @@
|
||||||
"message": "Add Style",
|
"message": "Add Style",
|
||||||
"description": "Title of the page for adding styles"
|
"description": "Title of the page for adding styles"
|
||||||
},
|
},
|
||||||
|
"alphaChannel": {
|
||||||
|
"message": "Opacity",
|
||||||
|
"description": "Label of color's opacity"
|
||||||
|
},
|
||||||
"appliesAdd": {
|
"appliesAdd": {
|
||||||
"message": "Add",
|
"message": "Add",
|
||||||
"description": "Label for the button to add an 'applies' entry"
|
"description": "Label for the button to add an 'applies' entry"
|
||||||
|
@ -36,6 +40,14 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
"appliesLineWidgetLabel": {
|
||||||
|
"message": "Display 'Applies to' info",
|
||||||
|
"description": "Label for the checkbox to display applies-to information in the single editor"
|
||||||
|
},
|
||||||
|
"appliesLineWidgetWarning": {
|
||||||
|
"message": "Does not work with minified CSS",
|
||||||
|
"description": "A warning that applies-to information won't show properly with minified CSS"
|
||||||
|
},
|
||||||
"appliesRegexpOption": {
|
"appliesRegexpOption": {
|
||||||
"message": "URLs matching the regexp",
|
"message": "URLs matching the regexp",
|
||||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||||
|
@ -44,6 +56,10 @@
|
||||||
"message": "Remove",
|
"message": "Remove",
|
||||||
"description": "Label for the button to remove an 'applies' entry"
|
"description": "Label for the button to remove an 'applies' entry"
|
||||||
},
|
},
|
||||||
|
"appliesRemoveError": {
|
||||||
|
"message": "Can not remove last 'applies to' entry",
|
||||||
|
"description": "Error displayed when the last 'applies' is going to be removed"
|
||||||
|
},
|
||||||
"appliesSpecify": {
|
"appliesSpecify": {
|
||||||
"message": "Specify",
|
"message": "Specify",
|
||||||
"description": "Label for the button to make a style apply only to specific sites"
|
"description": "Label for the button to make a style apply only to specific sites"
|
||||||
|
@ -64,6 +80,10 @@
|
||||||
"message": "Apply all updates",
|
"message": "Apply all updates",
|
||||||
"description": "Label for the button to apply all detected updates"
|
"description": "Label for the button to apply all detected updates"
|
||||||
},
|
},
|
||||||
|
"author": {
|
||||||
|
"message": "Author",
|
||||||
|
"description": "Label for the style author"
|
||||||
|
},
|
||||||
"backupButtons": {
|
"backupButtons": {
|
||||||
"message": "Backup",
|
"message": "Backup",
|
||||||
"description": "Heading for backup"
|
"description": "Heading for backup"
|
||||||
|
@ -83,6 +103,10 @@
|
||||||
"updateCheckHistory": {
|
"updateCheckHistory": {
|
||||||
"message": "History of update checks"
|
"message": "History of update checks"
|
||||||
},
|
},
|
||||||
|
"configureStyle": {
|
||||||
|
"message": "Configure",
|
||||||
|
"description": "Label for the button to configure userstyle"
|
||||||
|
},
|
||||||
"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"
|
||||||
|
@ -167,6 +191,14 @@
|
||||||
"message": "No",
|
"message": "No",
|
||||||
"description": "'No' button in a confirm dialog"
|
"description": "'No' button in a confirm dialog"
|
||||||
},
|
},
|
||||||
|
"confirmDefault": {
|
||||||
|
"message": "Use default",
|
||||||
|
"description": "'Set to default' button in a confirm dialog"
|
||||||
|
},
|
||||||
|
"confirmSave": {
|
||||||
|
"message": "Save",
|
||||||
|
"description": "'Save' button in a confirm dialog"
|
||||||
|
},
|
||||||
"confirmStop": {
|
"confirmStop": {
|
||||||
"message": "Stop",
|
"message": "Stop",
|
||||||
"description": "'Stop' button in a confirm dialog"
|
"description": "'Stop' button in a confirm dialog"
|
||||||
|
@ -175,6 +207,10 @@
|
||||||
"message": "Yes",
|
"message": "Yes",
|
||||||
"description": "'Yes' button in a confirm dialog"
|
"description": "'Yes' button in a confirm dialog"
|
||||||
},
|
},
|
||||||
|
"confirmClose": {
|
||||||
|
"message": "Close",
|
||||||
|
"description": "'Close' button in a confirm dialog"
|
||||||
|
},
|
||||||
"dbError": {
|
"dbError": {
|
||||||
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
"message": "An error has occurred using the Stylus database. Would you like to visit a web page with possible solutions?",
|
||||||
"description": "Prompt when a DB error is encountered"
|
"description": "Prompt when a DB error is encountered"
|
||||||
|
@ -257,6 +293,26 @@
|
||||||
"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)"
|
||||||
},
|
},
|
||||||
|
"externalLink": {
|
||||||
|
"message": "External link",
|
||||||
|
"description": "Label for external links"
|
||||||
|
},
|
||||||
|
"externalHomepage": {
|
||||||
|
"message": "Homepage",
|
||||||
|
"description": "Label for the external link to style's homepage"
|
||||||
|
},
|
||||||
|
"externalSupport": {
|
||||||
|
"message": "Support",
|
||||||
|
"description": "Label for the external link to style's support site"
|
||||||
|
},
|
||||||
|
"externalFeedback": {
|
||||||
|
"message": "Feedback",
|
||||||
|
"description": "Label for the external link to send feedback for the style"
|
||||||
|
},
|
||||||
|
"externalUsercssDocument": {
|
||||||
|
"message": "Documentation for Usercss",
|
||||||
|
"description": "Label for the external link to usercss documentation"
|
||||||
|
},
|
||||||
"filteredStyles": {
|
"filteredStyles": {
|
||||||
"message": "$numShown$ shown of $numTotal$ total",
|
"message": "$numShown$ shown of $numTotal$ total",
|
||||||
"description": "TL note - make this message short",
|
"description": "TL note - make this message short",
|
||||||
|
@ -345,10 +401,43 @@
|
||||||
"message": "Discard contents of current style and overwrite it with the imported style",
|
"message": "Discard contents of current style and overwrite it with the imported style",
|
||||||
"description": "Label for the button to import and overwrite current style"
|
"description": "Label for the button to import and overwrite current style"
|
||||||
},
|
},
|
||||||
|
"installButton": {
|
||||||
|
"message": "Install",
|
||||||
|
"description": "Label for install button"
|
||||||
|
},
|
||||||
|
"installButtonInstalled": {
|
||||||
|
"message": "Installed",
|
||||||
|
"description": "Text displayed when the style is successfully installed"
|
||||||
|
},
|
||||||
|
"installButtonUpdate": {
|
||||||
|
"message": "Update",
|
||||||
|
"description": "Label for update button"
|
||||||
|
},
|
||||||
|
"installButtonReinstall": {
|
||||||
|
"message": "Reinstall",
|
||||||
|
"description": "Label for reinstall button"
|
||||||
|
},
|
||||||
"installUpdate": {
|
"installUpdate": {
|
||||||
"message": "Install update",
|
"message": "Install update",
|
||||||
"description": "Label for the button to install an update for a single style"
|
"description": "Label for the button to install an update for a single style"
|
||||||
},
|
},
|
||||||
|
"installUpdateFrom": {
|
||||||
|
"message": "Currently the style is updated from $url$",
|
||||||
|
"description": "Label to describe where the style gets update",
|
||||||
|
"placeholders": {
|
||||||
|
"url": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"installUpdateFromLabel": {
|
||||||
|
"message": "Check for updates",
|
||||||
|
"description": "Label for the checkbox to save current URL for update check"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"message": "License",
|
||||||
|
"description": "Label for the license"
|
||||||
|
},
|
||||||
"linterConfigPopupTitle": {
|
"linterConfigPopupTitle": {
|
||||||
"message": "Set $linter$ rules configuration",
|
"message": "Set $linter$ rules configuration",
|
||||||
"description": "Stylelint or CSSLint popup header",
|
"description": "Stylelint or CSSLint popup header",
|
||||||
|
@ -366,6 +455,15 @@
|
||||||
"message": "(Set rule as: 0 = disabled; 1 = warning; 2 = error)",
|
"message": "(Set rule as: 0 = disabled; 1 = warning; 2 = error)",
|
||||||
"description": "CSSLint rule config values"
|
"description": "CSSLint rule config values"
|
||||||
},
|
},
|
||||||
|
"linterCSSLintIncompatible": {
|
||||||
|
"message": "CSSLint doesn't support $preprocessorname$ preprocessor",
|
||||||
|
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
|
||||||
|
"placeholders": {
|
||||||
|
"preprocessorname": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"linterInvalidConfigError": {
|
"linterInvalidConfigError": {
|
||||||
"message": "Not saved due to these invalid configuration settings:",
|
"message": "Not saved due to these invalid configuration settings:",
|
||||||
"description": "Invalid linter config will show a message followed by a list of invalid entries"
|
"description": "Invalid linter config will show a message followed by a list of invalid entries"
|
||||||
|
@ -395,6 +493,14 @@
|
||||||
"message": "See a full list of rules",
|
"message": "See a full list of rules",
|
||||||
"description": "Stylelint or CSSLint rules label added immediately before a link"
|
"description": "Stylelint or CSSLint rules label added immediately before a link"
|
||||||
},
|
},
|
||||||
|
"liveReloadLabel": {
|
||||||
|
"message": "Live reload",
|
||||||
|
"description": "The label of live-reload feature"
|
||||||
|
},
|
||||||
|
"liveReloadError": {
|
||||||
|
"message": "An error occurred while watching the file",
|
||||||
|
"description": "The label of live-reload error"
|
||||||
|
},
|
||||||
"manageFilters": {
|
"manageFilters": {
|
||||||
"message": "Filters",
|
"message": "Filters",
|
||||||
"description": "Label for filters container"
|
"description": "Label for filters container"
|
||||||
|
@ -483,6 +589,10 @@
|
||||||
"message": "More Options",
|
"message": "More Options",
|
||||||
"description": "Subheading for options section on manage page."
|
"description": "Subheading for options section on manage page."
|
||||||
},
|
},
|
||||||
|
"parseUsercssError": {
|
||||||
|
"message": "Stylus failed to parse usercss:",
|
||||||
|
"description": "The error message to show when stylus failed to parse usercss"
|
||||||
|
},
|
||||||
"popupManageTooltip": {
|
"popupManageTooltip": {
|
||||||
"message": "Shift-click or right-click opens manager with styles applicable for current site",
|
"message": "Shift-click or right-click opens manager with styles applicable for current site",
|
||||||
"description": "Tooltip for the 'Manage' button in the popup."
|
"description": "Tooltip for the 'Manage' button in the popup."
|
||||||
|
@ -629,6 +739,65 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"styleInstallOverwrite" : {
|
||||||
|
"message": "'$stylename$' is already installed. Overwrite?\nVersion: $oldVersion$ -> $newVersion$",
|
||||||
|
"description": "Confirmation when re-installing a style",
|
||||||
|
"placeholders": {
|
||||||
|
"stylename": {
|
||||||
|
"content": "$1"
|
||||||
|
},
|
||||||
|
"oldVersion": {
|
||||||
|
"content": "$2"
|
||||||
|
},
|
||||||
|
"newVersion": {
|
||||||
|
"content": "$3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styleInstallFailed": {
|
||||||
|
"message": "Failed to install userstyle!\n$error$",
|
||||||
|
"description": "Warning when installation failed",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styleMetaErrorCheckbox": {
|
||||||
|
"message": "Invalid @var checkbox: value must be 0 or 1",
|
||||||
|
"description": "Error displayed when the value of @var checkbox is invalid"
|
||||||
|
},
|
||||||
|
"styleMetaErrorColor": {
|
||||||
|
"message": "$color$ is not a valid color",
|
||||||
|
"description": "Error displayed when the value of @var color is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"color": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styleMetaErrorPreprocessor": {
|
||||||
|
"message": "Unsupported @preprocessor: $preprocessor$",
|
||||||
|
"description": "Error displayed when the value of @preprocessor is not supported",
|
||||||
|
"placeholders": {
|
||||||
|
"preprocessor": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"styleMetaErrorSelectValueMismatch": {
|
||||||
|
"message": "Invalid @select: value doesn't exist in the list",
|
||||||
|
"description": "Error displayed when the value of @select is invalid"
|
||||||
|
},
|
||||||
|
"styleMissingMeta": {
|
||||||
|
"message": "Missing metadata @$key$",
|
||||||
|
"description": "Error displayed when a mandatory metadata is missing",
|
||||||
|
"placeholders": {
|
||||||
|
"key": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Enter a name.",
|
"message": "Enter a name.",
|
||||||
"description": "Error displayed when user saves without providing a name"
|
"description": "Error displayed when user saves without providing a name"
|
||||||
|
@ -645,6 +814,10 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
"styleFromMozillaFormatError": {
|
||||||
|
"message": "Failed to import from Mozilla format",
|
||||||
|
"description": "Label for the import error"
|
||||||
|
},
|
||||||
"styleFromMozillaFormatPrompt": {
|
"styleFromMozillaFormatPrompt": {
|
||||||
"message": "Paste the Mozilla-format code",
|
"message": "Paste the Mozilla-format code",
|
||||||
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
|
||||||
|
@ -666,6 +839,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"styleUpdateDiscardChanges": {
|
||||||
|
"message": "The style is changed outside of the editor. Would you like to reload the style?",
|
||||||
|
"description": "Confirmation to update the style in the editor"
|
||||||
|
},
|
||||||
"stylusUnavailableForURL": {
|
"stylusUnavailableForURL": {
|
||||||
"message": "Stylus doesn't work on pages like this.",
|
"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"
|
||||||
|
@ -743,6 +920,10 @@
|
||||||
"message": "Updates installed:",
|
"message": "Updates installed:",
|
||||||
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
||||||
},
|
},
|
||||||
|
"versionInvalidOlder": {
|
||||||
|
"message": "The version is older than the installed style.",
|
||||||
|
"description": "Displayed when the version of style is older than the installed one"
|
||||||
|
},
|
||||||
"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"
|
||||||
|
@ -805,6 +986,9 @@
|
||||||
"optionsAdvancedContextDelete": {
|
"optionsAdvancedContextDelete": {
|
||||||
"message": "Add 'Delete' in editor context menu"
|
"message": "Add 'Delete' in editor context menu"
|
||||||
},
|
},
|
||||||
|
"optionsAdvancedNewStyleAsUsercss": {
|
||||||
|
"message": "Write new style as usercss"
|
||||||
|
},
|
||||||
"optionsActions": {
|
"optionsActions": {
|
||||||
"message": "Actions"
|
"message": "Actions"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* global dbExec, getStyles, saveStyle */
|
/* global dbExec, getStyles, saveStyle */
|
||||||
/* global handleCssTransitionBug */
|
/* global handleCssTransitionBug */
|
||||||
|
/* global usercssHelper openEditor */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -302,6 +303,14 @@ function onRuntimeMessage(request, sender, sendResponse) {
|
||||||
saveStyle(request).then(sendResponse);
|
saveStyle(request).then(sendResponse);
|
||||||
return KEEP_CHANNEL_OPEN;
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'saveUsercss':
|
||||||
|
usercssHelper.save(request, true).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'buildUsercss':
|
||||||
|
usercssHelper.build(request, true).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
case 'healthCheck':
|
case 'healthCheck':
|
||||||
dbExec()
|
dbExec()
|
||||||
.then(() => sendResponse(true))
|
.then(() => sendResponse(true))
|
||||||
|
@ -313,5 +322,36 @@ function onRuntimeMessage(request, sender, sendResponse) {
|
||||||
.then(sendResponse)
|
.then(sendResponse)
|
||||||
.catch(() => sendResponse(null));
|
.catch(() => sendResponse(null));
|
||||||
return KEEP_CHANNEL_OPEN;
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'openUsercssInstallPage':
|
||||||
|
usercssHelper.openInstallPage(sender.tab.id, request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'closeTab':
|
||||||
|
closeTab(sender.tab.id, request).then(sendResponse);
|
||||||
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
|
case 'openEditor':
|
||||||
|
openEditor(request.id);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeTab(tabId, request) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (request.tabId) {
|
||||||
|
tabId = request.tabId;
|
||||||
|
}
|
||||||
|
chrome.tabs.remove(tabId, () => {
|
||||||
|
const {lastError} = chrome.runtime;
|
||||||
|
if (lastError) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: lastError.message || String(lastError)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({success: true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -383,12 +383,21 @@ function saveStyle(style) {
|
||||||
}
|
}
|
||||||
let existed;
|
let existed;
|
||||||
let codeIsUpdated;
|
let codeIsUpdated;
|
||||||
|
|
||||||
|
return maybeCalcDigest()
|
||||||
|
.then(maybeImportFix)
|
||||||
|
.then(decide);
|
||||||
|
|
||||||
|
function maybeCalcDigest() {
|
||||||
if (reason === 'update' || reason === 'update-digest') {
|
if (reason === 'update' || reason === 'update-digest') {
|
||||||
return calcStyleDigest(style).then(digest => {
|
return calcStyleDigest(style).then(digest => {
|
||||||
style.originalDigest = digest;
|
style.originalDigest = digest;
|
||||||
return decide();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeImportFix() {
|
||||||
if (reason === 'import') {
|
if (reason === 'import') {
|
||||||
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
||||||
delete style.styleDigest; // TODO: remove in the future
|
delete style.styleDigest; // TODO: remove in the future
|
||||||
|
@ -396,7 +405,7 @@ function saveStyle(style) {
|
||||||
delete style.originalDigest;
|
delete style.originalDigest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return decide();
|
}
|
||||||
|
|
||||||
function decide() {
|
function decide() {
|
||||||
if (id !== null) {
|
if (id !== null) {
|
||||||
|
@ -714,7 +723,8 @@ function normalizeStyleSections({sections}) {
|
||||||
|
|
||||||
|
|
||||||
function calcStyleDigest(style) {
|
function calcStyleDigest(style) {
|
||||||
const jsonString = JSON.stringify(normalizeStyleSections(style));
|
const jsonString = style.usercssData ?
|
||||||
|
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
||||||
const text = new TextEncoder('utf-8').encode(jsonString);
|
const text = new TextEncoder('utf-8').encode(jsonString);
|
||||||
return crypto.subtle.digest('SHA-1', text).then(hex);
|
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||||
/* global calcStyleDigest */
|
/* global calcStyleDigest */
|
||||||
|
/* global usercss semverCompare usercssHelper */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -15,8 +16,10 @@ var updater = {
|
||||||
MAYBE_EDITED: 'may be locally edited',
|
MAYBE_EDITED: 'may be locally edited',
|
||||||
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||||
SAME_CODE: 'up-to-date: code sections are unchanged',
|
SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||||
|
SAME_VERSION: 'up-to-date: version is unchanged',
|
||||||
ERROR_MD5: 'error: MD5 is invalid',
|
ERROR_MD5: 'error: MD5 is invalid',
|
||||||
ERROR_JSON: 'error: JSON is invalid',
|
ERROR_JSON: 'error: JSON is invalid',
|
||||||
|
ERROR_VERSION: 'error: version is older than installed style',
|
||||||
|
|
||||||
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
||||||
|
|
||||||
|
@ -53,9 +56,11 @@ var updater = {
|
||||||
|
|
||||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||||
*/
|
*/
|
||||||
|
const maybeUpdate = style.usercssData ? maybeUpdateUsercss : maybeUpdateUSO;
|
||||||
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
|
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
|
||||||
.then(maybeFetchMd5)
|
.then(checkIfEdited)
|
||||||
.then(maybeFetchCode)
|
.then(maybeUpdate)
|
||||||
|
.then(maybeValidate)
|
||||||
.then(maybeSave)
|
.then(maybeSave)
|
||||||
.then(saved => {
|
.then(saved => {
|
||||||
observer(updater.UPDATED, saved);
|
observer(updater.UPDATED, saved);
|
||||||
|
@ -67,42 +72,79 @@ var updater = {
|
||||||
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function maybeFetchMd5(digest) {
|
function checkIfEdited(digest) {
|
||||||
if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) {
|
if (ignoreDigest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (style.originalDigest && style.originalDigest !== digest) {
|
||||||
return Promise.reject(updater.EDITED);
|
return Promise.reject(updater.EDITED);
|
||||||
}
|
}
|
||||||
return download(style.md5Url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeFetchCode(md5) {
|
function maybeUpdateUSO() {
|
||||||
|
return download(style.md5Url).then(md5 => {
|
||||||
if (!md5 || md5.length !== 32) {
|
if (!md5 || md5.length !== 32) {
|
||||||
return Promise.reject(updater.ERROR_MD5);
|
return Promise.reject(updater.ERROR_MD5);
|
||||||
}
|
}
|
||||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||||
return Promise.reject(updater.SAME_MD5);
|
return Promise.reject(updater.SAME_MD5);
|
||||||
}
|
}
|
||||||
return download(style.updateUrl);
|
return download(style.updateUrl)
|
||||||
|
.then(text => tryJSONparse(text));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeSave(text) {
|
function maybeUpdateUsercss() {
|
||||||
const json = tryJSONparse(text);
|
return download(style.updateUrl).then(text => {
|
||||||
|
const json = usercss.buildMeta(text);
|
||||||
|
const {usercssData: {version}} = style;
|
||||||
|
const {usercssData: {version: newVersion}} = json;
|
||||||
|
switch (Math.sign(semverCompare(version, newVersion))) {
|
||||||
|
case 0:
|
||||||
|
// re-install is invalid in a soft upgrade
|
||||||
|
if (!ignoreDigest) {
|
||||||
|
return Promise.reject(updater.SAME_VERSION);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// downgrade is always invalid
|
||||||
|
return Promise.reject(updater.ERROR_VERSION);
|
||||||
|
}
|
||||||
|
return usercss.buildCode(json);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeValidate(json) {
|
||||||
|
if (json.usercssData) {
|
||||||
|
// usercss is already validated while building
|
||||||
|
return json;
|
||||||
|
}
|
||||||
if (!styleJSONseemsValid(json)) {
|
if (!styleJSONseemsValid(json)) {
|
||||||
return Promise.reject(updater.ERROR_JSON);
|
return Promise.reject(updater.ERROR_JSON);
|
||||||
}
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeSave(json) {
|
||||||
json.id = style.id;
|
json.id = style.id;
|
||||||
if (styleSectionsEqual(json, style)) {
|
if (styleSectionsEqual(json, style)) {
|
||||||
// JSONs may have different order of items even if sections are effectively equal
|
// JSONs may have different order of items even if sections are effectively equal
|
||||||
// so we'll update the digest anyway
|
// so we'll update the digest anyway
|
||||||
|
// always update digest even if (save === false)
|
||||||
saveStyle(Object.assign(json, {reason: 'update-digest'}));
|
saveStyle(Object.assign(json, {reason: 'update-digest'}));
|
||||||
return Promise.reject(updater.SAME_CODE);
|
return Promise.reject(updater.SAME_CODE);
|
||||||
} else if (!style.originalDigest && !ignoreDigest) {
|
} else if (!style.originalDigest && !ignoreDigest) {
|
||||||
return Promise.reject(updater.MAYBE_EDITED);
|
return Promise.reject(updater.MAYBE_EDITED);
|
||||||
}
|
}
|
||||||
return !save ? json :
|
if (!save) {
|
||||||
saveStyle(Object.assign(json, {
|
return json;
|
||||||
name: null, // keep local name customizations
|
}
|
||||||
reason: 'update',
|
json.reason = 'update';
|
||||||
}));
|
if (json.usercssData) {
|
||||||
|
return usercssHelper.save(json);
|
||||||
|
}
|
||||||
|
json.name = null; // keep local name customizations
|
||||||
|
return saveStyle(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleJSONseemsValid(json) {
|
function styleJSONseemsValid(json) {
|
||||||
|
|
89
background/usercss-helper.js
Normal file
89
background/usercss-helper.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/* global usercss saveStyle getStyles */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var usercssHelper = (() => {
|
||||||
|
function buildMeta(style) {
|
||||||
|
if (style.usercssData) {
|
||||||
|
return Promise.resolve(style);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {sourceCode} = style;
|
||||||
|
// allow sourceCode to be normalized
|
||||||
|
delete style.sourceCode;
|
||||||
|
return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style));
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCode(style) {
|
||||||
|
return usercss.buildCode(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapReject(pending) {
|
||||||
|
return pending.then(result => ({success: true, result}))
|
||||||
|
.catch(err => ({success: false, result: err.message || String(err)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the source and find the duplication
|
||||||
|
function build({sourceCode, checkDup = false}, noReject) {
|
||||||
|
const pending = buildMeta({sourceCode})
|
||||||
|
.then(style => Promise.all([
|
||||||
|
buildCode(style),
|
||||||
|
checkDup && findDup(style)
|
||||||
|
]))
|
||||||
|
.then(([style, dup]) => ({style, dup}));
|
||||||
|
|
||||||
|
return noReject ? wrapReject(pending) : pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(style, noReject) {
|
||||||
|
const pending = buildMeta(style)
|
||||||
|
.then(assignVars)
|
||||||
|
.then(buildCode)
|
||||||
|
.then(saveStyle);
|
||||||
|
|
||||||
|
return noReject ? wrapReject(pending) : pending;
|
||||||
|
|
||||||
|
function assignVars(style) {
|
||||||
|
if (style.reason === 'config' && style.id) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
return findDup(style).then(dup => {
|
||||||
|
if (dup) {
|
||||||
|
style.id = dup.id;
|
||||||
|
if (style.reason !== 'config') {
|
||||||
|
// preserve style.vars during update
|
||||||
|
usercss.assignVars(style, dup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDup(style) {
|
||||||
|
if (style.id) {
|
||||||
|
return getStyles({id: style.id}).then(s => s[0]);
|
||||||
|
}
|
||||||
|
return getStyles().then(styles =>
|
||||||
|
styles.find(target => {
|
||||||
|
if (!target.usercssData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return target.usercssData.name === style.usercssData.name &&
|
||||||
|
target.usercssData.namespace === style.usercssData.namespace;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInstallPage(tabId, request) {
|
||||||
|
const url = '/install-usercss.html' +
|
||||||
|
'?updateUrl=' + encodeURIComponent(request.updateUrl) +
|
||||||
|
'&tabId=' + tabId;
|
||||||
|
return wrapReject(openURL({url}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {build, save, findDup, openInstallPage};
|
||||||
|
})();
|
122
content/install-user-css.js
Normal file
122
content/install-user-css.js
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/* global runtimeSend */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createSourceLoader() {
|
||||||
|
let source;
|
||||||
|
|
||||||
|
function fetchText(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// you can't use fetch in Chrome under 'file:' protocol
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url);
|
||||||
|
xhr.addEventListener('load', () => resolve(xhr.responseText));
|
||||||
|
xhr.addEventListener('error', () => reject(xhr));
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
return fetchText(location.href).then(newSource => {
|
||||||
|
source = newSource;
|
||||||
|
return source;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch(cb) {
|
||||||
|
let timer;
|
||||||
|
const DELAY = 1000;
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (timer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timer = setTimeout(check, DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
fetchText(location.href)
|
||||||
|
.then(newSource => {
|
||||||
|
if (source !== newSource) {
|
||||||
|
source = newSource;
|
||||||
|
return cb(source);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(t('liveReloadError', error));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
timer = setTimeout(check, DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {start, stop};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {load, watch, source: () => source};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initUsercssInstall() {
|
||||||
|
const sourceLoader = createSourceLoader();
|
||||||
|
const pendingSource = sourceLoader.load();
|
||||||
|
let watcher;
|
||||||
|
|
||||||
|
chrome.runtime.onConnect.addListener(port => {
|
||||||
|
// FIXME: is this the correct way to reject a connection?
|
||||||
|
// https://developer.chrome.com/extensions/messaging#connect
|
||||||
|
console.assert(port.name === 'usercss-install');
|
||||||
|
|
||||||
|
port.onMessage.addListener(msg => {
|
||||||
|
switch (msg.method) {
|
||||||
|
case 'getSourceCode':
|
||||||
|
pendingSource
|
||||||
|
.then(sourceCode => port.postMessage({method: msg.method + 'Response', sourceCode}))
|
||||||
|
.catch(err => port.postMessage({method: msg.method + 'Response', error: err.message || String(err)}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'liveReloadStart':
|
||||||
|
if (!watcher) {
|
||||||
|
watcher = sourceLoader.watch(sourceCode => {
|
||||||
|
port.postMessage({method: 'sourceCodeChanged', sourceCode});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
watcher.start();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'liveReloadStop':
|
||||||
|
watcher.stop();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'closeTab':
|
||||||
|
if (history.length > 1) {
|
||||||
|
history.back();
|
||||||
|
} else {
|
||||||
|
runtimeSend({method: 'closeTab'});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return runtimeSend({
|
||||||
|
method: 'openUsercssInstallPage',
|
||||||
|
updateUrl: location.href
|
||||||
|
}).catch(alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUsercss() {
|
||||||
|
if (!/text\/(css|plain)/.test(document.contentType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/==userstyle==/i.test(document.body.textContent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUsercss()) {
|
||||||
|
initUsercssInstall();
|
||||||
|
}
|
10
content/util.js
Normal file
10
content/util.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function runtimeSend(request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
request,
|
||||||
|
({success, result}) => (success ? resolve : reject)(result)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
11
edit.html
11
edit.html
|
@ -17,9 +17,15 @@
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
|
<script src="js/script-loader.js"></script>
|
||||||
|
<script src="js/moz-parser.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
<link rel="stylesheet" href="edit/edit.css">
|
<link rel="stylesheet" href="edit/edit.css">
|
||||||
<script src="edit/lint.js"></script>
|
<script src="edit/lint.js"></script>
|
||||||
|
<script src="edit/util.js"></script>
|
||||||
|
<script src="edit/regexp-tester.js"></script>
|
||||||
|
<script src="edit/applies-to-line-widget.js"></script>
|
||||||
|
<script src="edit/source-editor.js"></script>
|
||||||
<script src="edit/edit.js"></script>
|
<script src="edit/edit.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||||
|
@ -50,9 +56,14 @@
|
||||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||||
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||||
|
|
||||||
|
<script src="vendor/codemirror/addon/mode/loadmode.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
||||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
<script src="vendor/codemirror/keymap/vim.js"></script>
|
||||||
|
|
||||||
|
<script src="/edit/codemirror-default.js"></script>
|
||||||
|
<link rel="stylesheet" href="/edit/codemirror-default.css">
|
||||||
<link id="cm-theme" rel="stylesheet">
|
<link id="cm-theme" rel="stylesheet">
|
||||||
|
|
||||||
<template data-id="appliesTo">
|
<template data-id="appliesTo">
|
||||||
|
|
408
edit/applies-to-line-widget.js
Normal file
408
edit/applies-to-line-widget.js
Normal file
|
@ -0,0 +1,408 @@
|
||||||
|
/* global regExpTester debounce messageBox */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createAppliesToLineWidget(cm) {
|
||||||
|
const APPLIES_TYPE = [
|
||||||
|
[t('appliesUrlOption'), 'url'],
|
||||||
|
[t('appliesUrlPrefixOption'), 'url-prefix'],
|
||||||
|
[t('appliesDomainOption'), 'domain'],
|
||||||
|
[t('appliesRegexpOption'), 'regexp']
|
||||||
|
];
|
||||||
|
const THROTTLE_DELAY = 400;
|
||||||
|
let widgets = [];
|
||||||
|
let fromLine, toLine, gutterStyle;
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
return {toggle};
|
||||||
|
|
||||||
|
function toggle(newState = !initialized) {
|
||||||
|
newState = Boolean(newState);
|
||||||
|
if (newState !== initialized) {
|
||||||
|
if (newState) {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
uninit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
gutterStyle = getComputedStyle(cm.getGutterElement());
|
||||||
|
fromLine = null;
|
||||||
|
toLine = null;
|
||||||
|
|
||||||
|
cm.on('change', onChange);
|
||||||
|
cm.on('optionChange', onOptionChange);
|
||||||
|
|
||||||
|
// is it possible to avoid flickering?
|
||||||
|
window.addEventListener('load', updateWidgetStyle);
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninit() {
|
||||||
|
initialized = false;
|
||||||
|
|
||||||
|
widgets.forEach(clearWidget);
|
||||||
|
widgets.length = 0;
|
||||||
|
cm.off('change', onChange);
|
||||||
|
cm.off('optionChange', onOptionChange);
|
||||||
|
window.removeEventListener('load', updateWidgetStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(cm, {from, to, origin}) {
|
||||||
|
if (origin === 'appliesTo') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fromLine === null || toLine === null) {
|
||||||
|
fromLine = from.line;
|
||||||
|
toLine = to.line;
|
||||||
|
} else {
|
||||||
|
fromLine = Math.min(fromLine, from.line);
|
||||||
|
toLine = Math.max(toLine, to.line);
|
||||||
|
}
|
||||||
|
debounce(update, THROTTLE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOptionChange(cm, option) {
|
||||||
|
if (option === 'theme') {
|
||||||
|
updateWidgetStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
cm.operation(doUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidgetStyle() {
|
||||||
|
gutterStyle = getComputedStyle(cm.getGutterElement());
|
||||||
|
widgets.forEach(setWidgetStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWidgetStyle(widget) {
|
||||||
|
let borderStyle = '';
|
||||||
|
if (gutterStyle.borderRightWidth !== '0px') {
|
||||||
|
borderStyle = `${gutterStyle.borderRightWidth} ${gutterStyle.borderRightStyle} ${gutterStyle.borderRightColor}`;
|
||||||
|
} else {
|
||||||
|
borderStyle = `1px solid ${gutterStyle.color}`;
|
||||||
|
}
|
||||||
|
widget.node.style.backgroundColor = gutterStyle.backgroundColor;
|
||||||
|
widget.node.style.borderTop = borderStyle;
|
||||||
|
widget.node.style.borderBottom = borderStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doUpdate() {
|
||||||
|
// find which widgets needs to be update
|
||||||
|
// some widgets (lines) might be deleted
|
||||||
|
widgets = widgets.filter(w => w.line.lineNo() !== null);
|
||||||
|
let i = fromLine === null ? 0 : widgets.findIndex(w => w.line.lineNo() > fromLine) - 1;
|
||||||
|
let j = toLine === null ? 0 : widgets.findIndex(w => w.line.lineNo() > toLine);
|
||||||
|
if (i === -2) {
|
||||||
|
i = widgets.length - 1;
|
||||||
|
}
|
||||||
|
if (j < 0) {
|
||||||
|
j = widgets.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decide search range
|
||||||
|
const fromIndex = widgets[i] ? cm.indexFromPos({line: widgets[i].line.lineNo(), ch: 0}) : 0;
|
||||||
|
const toIndex = widgets[j] ? cm.indexFromPos({line: widgets[j].line.lineNo(), ch: 0}) : cm.getValue().length;
|
||||||
|
|
||||||
|
// splice
|
||||||
|
i = Math.max(0, i);
|
||||||
|
widgets.splice(i, 0, ...createWidgets(fromIndex, toIndex, widgets.splice(i, j - i)));
|
||||||
|
|
||||||
|
fromLine = null;
|
||||||
|
toLine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function *createWidgets(start, end, removed) {
|
||||||
|
let i = 0;
|
||||||
|
for (const section of findAppliesTo(start, end)) {
|
||||||
|
while (removed[i] && removed[i].line.lineNo() < section.pos.line) {
|
||||||
|
clearWidget(removed[i++]);
|
||||||
|
}
|
||||||
|
setupMarkers(section);
|
||||||
|
if (removed[i] && removed[i].line.lineNo() === section.pos.line) {
|
||||||
|
// reuse old widget
|
||||||
|
removed[i].section.applies.forEach(apply => {
|
||||||
|
apply.type.mark.clear();
|
||||||
|
apply.value.mark.clear();
|
||||||
|
});
|
||||||
|
removed[i].section = section;
|
||||||
|
const newNode = buildElement(section);
|
||||||
|
removed[i].node.parentNode.replaceChild(newNode, removed[i].node);
|
||||||
|
removed[i].node = newNode;
|
||||||
|
setWidgetStyle(removed[i]);
|
||||||
|
removed[i].changed();
|
||||||
|
yield removed[i];
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// new widget
|
||||||
|
const widget = cm.addLineWidget(section.pos.line, buildElement(section), {
|
||||||
|
coverGutter: true,
|
||||||
|
noHScroll: true,
|
||||||
|
above: true
|
||||||
|
});
|
||||||
|
widget.section = section;
|
||||||
|
setWidgetStyle(widget);
|
||||||
|
yield widget;
|
||||||
|
}
|
||||||
|
removed.slice(i).forEach(clearWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearWidget(widget) {
|
||||||
|
widget.clear();
|
||||||
|
widget.section.applies.forEach(clearApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearApply(apply) {
|
||||||
|
apply.type.mark.clear();
|
||||||
|
apply.value.mark.clear();
|
||||||
|
apply.mark.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMarkers({applies}) {
|
||||||
|
applies.forEach(setupApplyMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupApplyMarkers(apply) {
|
||||||
|
apply.type.mark = cm.markText(
|
||||||
|
cm.posFromIndex(apply.type.start),
|
||||||
|
cm.posFromIndex(apply.type.end),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
apply.value.mark = cm.markText(
|
||||||
|
cm.posFromIndex(apply.value.start),
|
||||||
|
cm.posFromIndex(apply.value.end),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
apply.mark = cm.markText(
|
||||||
|
cm.posFromIndex(apply.start),
|
||||||
|
cm.posFromIndex(apply.end),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildElement({applies}) {
|
||||||
|
const el = $element({className: 'applies-to', appendChild: [
|
||||||
|
$element({tag: 'label', appendChild: [
|
||||||
|
t('appliesLabel'),
|
||||||
|
// $element({tag: 'svg'})
|
||||||
|
]}),
|
||||||
|
$element({
|
||||||
|
tag: 'ul',
|
||||||
|
className: 'applies-to-list',
|
||||||
|
appendChild: applies.map(makeLi)
|
||||||
|
})
|
||||||
|
]});
|
||||||
|
if (!$('li', el)) {
|
||||||
|
$('ul', el).appendChild($element({
|
||||||
|
tag: 'li',
|
||||||
|
className: 'applies-to-everything',
|
||||||
|
textContent: t('appliesToEverything')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
|
||||||
|
function makeLi(apply) {
|
||||||
|
const el = $element({tag: 'li', appendChild: makeInput(apply)});
|
||||||
|
el.dataset.type = apply.type.text;
|
||||||
|
el.addEventListener('change', e => {
|
||||||
|
if (e.target.classList.contains('applies-type')) {
|
||||||
|
el.dataset.type = apply.type.text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeInput(apply) {
|
||||||
|
const typeInput = $element({
|
||||||
|
tag: 'select',
|
||||||
|
className: 'applies-type',
|
||||||
|
appendChild: APPLIES_TYPE.map(([label, value]) => $element({
|
||||||
|
tag: 'option',
|
||||||
|
value: value,
|
||||||
|
textContent: label
|
||||||
|
})),
|
||||||
|
onchange() {
|
||||||
|
applyChange(apply.type, this.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
typeInput.value = apply.type.text;
|
||||||
|
const valueInput = $element({
|
||||||
|
tag: 'input',
|
||||||
|
className: 'applies-value',
|
||||||
|
value: apply.value.text,
|
||||||
|
oninput() {
|
||||||
|
debounce(applyChange, THROTTLE_DELAY, apply.value, this.value);
|
||||||
|
},
|
||||||
|
onfocus: updateRegexpTest
|
||||||
|
});
|
||||||
|
const regexpTestButton = $element({
|
||||||
|
tag: 'button',
|
||||||
|
type: 'button',
|
||||||
|
className: 'applies-to-regexp-test',
|
||||||
|
textContent: t('styleRegexpTestButton'),
|
||||||
|
onclick() {
|
||||||
|
regExpTester.toggle();
|
||||||
|
regExpTester.update([apply.value.text]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const removeButton = $element({
|
||||||
|
tag: 'button',
|
||||||
|
type: 'button',
|
||||||
|
className: 'applies-to-remove',
|
||||||
|
textContent: t('appliesRemove'),
|
||||||
|
onclick() {
|
||||||
|
const i = applies.indexOf(apply);
|
||||||
|
let repl;
|
||||||
|
let from;
|
||||||
|
let to;
|
||||||
|
if (applies.length < 2) {
|
||||||
|
messageBox({
|
||||||
|
contents: chrome.i18n.getMessage('appliesRemoveError'),
|
||||||
|
buttons: [t('confirmClose')]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (i === 0) {
|
||||||
|
from = apply.mark.find().from;
|
||||||
|
to = applies[i + 1].mark.find().from;
|
||||||
|
repl = '';
|
||||||
|
} else if (i === applies.length - 1) {
|
||||||
|
from = applies[i - 1].mark.find().to;
|
||||||
|
to = apply.mark.find().to;
|
||||||
|
repl = '';
|
||||||
|
} else {
|
||||||
|
from = applies[i - 1].mark.find().to;
|
||||||
|
to = applies[i + 1].mark.find().from;
|
||||||
|
repl = ', ';
|
||||||
|
}
|
||||||
|
cm.replaceRange(repl, from, to, 'appliesTo');
|
||||||
|
clearApply(apply);
|
||||||
|
this.closest('li').remove();
|
||||||
|
applies.splice(i, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const addButton = $element({
|
||||||
|
tag: 'button',
|
||||||
|
type: 'button',
|
||||||
|
className: 'applies-to-add',
|
||||||
|
textContent: t('appliesAdd'),
|
||||||
|
onclick() {
|
||||||
|
const i = applies.indexOf(apply);
|
||||||
|
const pos = apply.mark.find().to;
|
||||||
|
const text = `, ${apply.type.text}("")`;
|
||||||
|
cm.replaceRange(text, pos, pos, 'appliesTo');
|
||||||
|
const newApply = createApply(
|
||||||
|
cm.indexFromPos(pos) + 2,
|
||||||
|
apply.type.text,
|
||||||
|
'',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
setupApplyMarkers(newApply);
|
||||||
|
applies.splice(i + 1, 0, newApply);
|
||||||
|
this.closest('li').insertAdjacentElement('afterend', makeLi(newApply));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [typeInput, valueInput, regexpTestButton, removeButton, addButton];
|
||||||
|
|
||||||
|
function updateRegexpTest() {
|
||||||
|
if (apply.type.text === 'regexp') {
|
||||||
|
const re = apply.value.text.trim();
|
||||||
|
if (re) {
|
||||||
|
regExpTester.update([re]);
|
||||||
|
} else {
|
||||||
|
regExpTester.update([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyChange(input, newText) {
|
||||||
|
const range = input.mark.find();
|
||||||
|
input.mark.clear();
|
||||||
|
cm.replaceRange(newText, range.from, range.to, 'appliesTo');
|
||||||
|
input.mark = cm.markText(
|
||||||
|
range.from,
|
||||||
|
cm.findPosH(range.from, newText.length, 'char'),
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
input.text = newText;
|
||||||
|
|
||||||
|
if (input === apply.type) {
|
||||||
|
const range = apply.mark.find();
|
||||||
|
apply.mark.clear();
|
||||||
|
apply.mark = cm.markText(
|
||||||
|
input.mark.find().from,
|
||||||
|
range.to,
|
||||||
|
{clearWhenEmpty: false}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRegexpTest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApply(pos, typeText, valueText, isQuoted = false) {
|
||||||
|
const start = pos;
|
||||||
|
const typeStart = start;
|
||||||
|
const typeEnd = typeStart + typeText.length;
|
||||||
|
const valueStart = typeEnd + 1 + Number(isQuoted);
|
||||||
|
const valueEnd = valueStart + valueText.length;
|
||||||
|
const end = valueEnd + Number(isQuoted) + 1;
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
type: {
|
||||||
|
text: typeText,
|
||||||
|
start: typeStart,
|
||||||
|
end: typeEnd,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
text: valueText,
|
||||||
|
start: valueStart,
|
||||||
|
end: valueEnd,
|
||||||
|
},
|
||||||
|
end
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function *findAppliesTo(posStart, posEnd) {
|
||||||
|
const text = cm.getValue();
|
||||||
|
const re = /^[\t ]*@-moz-document\s+/mg;
|
||||||
|
const applyRe = /(url|url-prefix|domain|regexp)\(((['"])(?:\\\\|\\\n|\\\3|[^\n])*?\3|[^)\n]*)\)[\s,]*/iyg;
|
||||||
|
let match;
|
||||||
|
re.lastIndex = posStart;
|
||||||
|
while ((match = re.exec(text))) {
|
||||||
|
if (match.index >= posEnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const applies = [];
|
||||||
|
let m;
|
||||||
|
applyRe.lastIndex = re.lastIndex;
|
||||||
|
while ((m = applyRe.exec(text))) {
|
||||||
|
const apply = createApply(
|
||||||
|
m.index,
|
||||||
|
m[1],
|
||||||
|
unquote(m[2]),
|
||||||
|
unquote(m[2]) !== m[2]
|
||||||
|
);
|
||||||
|
applies.push(apply);
|
||||||
|
re.lastIndex = applyRe.lastIndex;
|
||||||
|
}
|
||||||
|
yield {
|
||||||
|
pos: cm.posFromIndex(match.index),
|
||||||
|
applies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unquote(s) {
|
||||||
|
const first = s.charAt(0);
|
||||||
|
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
|
||||||
|
}
|
||||||
|
}
|
26
edit/codemirror-default.css
Normal file
26
edit/codemirror-default.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.CodeMirror-hint:hover {
|
||||||
|
color: white;
|
||||||
|
background: #08f;
|
||||||
|
}
|
||||||
|
.CodeMirror {
|
||||||
|
border: solid #CCC 1px;
|
||||||
|
}
|
||||||
|
.CodeMirror-lint-mark-warning {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-dialog {
|
||||||
|
-webkit-animation: highlight 3s ease-out;
|
||||||
|
}
|
||||||
|
.CodeMirror-focused {
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
.CodeMirror-search-field {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
.CodeMirror-jump-field {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
.CodeMirror-search-hint {
|
||||||
|
color: #888;
|
||||||
|
}
|
114
edit/codemirror-default.js
Normal file
114
edit/codemirror-default.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/* global CodeMirror prefs */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
||||||
|
if (!prefs.get('editor.keyMap')) {
|
||||||
|
prefs.reset('editor.keyMap');
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
mode: 'css',
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: [
|
||||||
|
'CodeMirror-linenumbers',
|
||||||
|
'CodeMirror-foldgutter',
|
||||||
|
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
||||||
|
],
|
||||||
|
matchBrackets: true,
|
||||||
|
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
|
||||||
|
hintOptions: {},
|
||||||
|
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
||||||
|
styleActiveLine: true,
|
||||||
|
theme: 'default',
|
||||||
|
keyMap: prefs.get('editor.keyMap'),
|
||||||
|
extraKeys: {
|
||||||
|
// independent of current keyMap
|
||||||
|
'Alt-Enter': 'toggleStyle',
|
||||||
|
'Alt-PageDown': 'nextEditor',
|
||||||
|
'Alt-PageUp': 'prevEditor'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
||||||
|
|
||||||
|
CodeMirror.commands.blockComment = cm => {
|
||||||
|
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'basic' keymap only has basic keys by design, so we skip it
|
||||||
|
|
||||||
|
const extraKeysCommands = {};
|
||||||
|
Object.keys(CodeMirror.defaults.extraKeys).forEach(key => {
|
||||||
|
extraKeysCommands[CodeMirror.defaults.extraKeys[key]] = true;
|
||||||
|
});
|
||||||
|
if (!extraKeysCommands.jumpToLine) {
|
||||||
|
CodeMirror.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
|
||||||
|
CodeMirror.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
|
||||||
|
CodeMirror.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
|
||||||
|
CodeMirror.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
|
||||||
|
}
|
||||||
|
if (!extraKeysCommands.autocomplete) {
|
||||||
|
// will be used by 'sublime' on PC via fallthrough
|
||||||
|
CodeMirror.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete';
|
||||||
|
// OSX uses Ctrl-Space and Cmd-Space for something else
|
||||||
|
CodeMirror.keyMap.macDefault['Alt-Space'] = 'autocomplete';
|
||||||
|
// copied from 'emacs' keymap
|
||||||
|
CodeMirror.keyMap.emacsy['Alt-/'] = 'autocomplete';
|
||||||
|
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
||||||
|
}
|
||||||
|
if (!extraKeysCommands.blockComment) {
|
||||||
|
CodeMirror.keyMap.sublime['Shift-Ctrl-/'] = 'blockComment';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.appVersion.includes('Windows')) {
|
||||||
|
// 'pcDefault' keymap on Windows should have F3/Shift-F3
|
||||||
|
if (!extraKeysCommands.findNext) {
|
||||||
|
CodeMirror.keyMap.pcDefault['F3'] = 'findNext';
|
||||||
|
}
|
||||||
|
if (!extraKeysCommands.findPrev) {
|
||||||
|
CodeMirror.keyMap.pcDefault['Shift-F3'] = 'findPrev';
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
|
||||||
|
['N', 'T', 'W'].forEach(char => {
|
||||||
|
[
|
||||||
|
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
||||||
|
// Note: modifier order in CodeMirror is S-C-A
|
||||||
|
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']}
|
||||||
|
].forEach(remap => {
|
||||||
|
const oldKey = remap.from + char;
|
||||||
|
Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
|
||||||
|
const keyMap = CodeMirror.keyMap[keyMapName];
|
||||||
|
const command = keyMap[oldKey];
|
||||||
|
if (!command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
remap.to.some(newMod => {
|
||||||
|
const newKey = newMod + char;
|
||||||
|
if (!(newKey in keyMap)) {
|
||||||
|
delete keyMap[oldKey];
|
||||||
|
keyMap[newKey] = command;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.modeURL = '/vendor/codemirror/mode/%N/%N.js';
|
||||||
|
|
||||||
|
const MODE = {
|
||||||
|
stylus: 'stylus',
|
||||||
|
uso: 'css'
|
||||||
|
};
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('setPreprocessor', function (preprocessor) {
|
||||||
|
this.setOption('mode', MODE[preprocessor] || 'css');
|
||||||
|
CodeMirror.autoLoadMode(this, MODE[preprocessor] || 'css');
|
||||||
|
});
|
||||||
|
})();
|
|
@ -141,6 +141,10 @@ h2 .svg-icon, label .svg-icon {
|
||||||
content: "";
|
content: "";
|
||||||
opacity: .15;
|
opacity: .15;
|
||||||
}
|
}
|
||||||
|
/* footer */
|
||||||
|
#footer {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
/************ content ***********/
|
/************ content ***********/
|
||||||
#sections > div {
|
#sections > div {
|
||||||
margin: 0.7rem;
|
margin: 0.7rem;
|
||||||
|
@ -174,18 +178,11 @@ h2 .svg-icon, label .svg-icon {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
/* code */
|
/* code */
|
||||||
.CodeMirror-hint:hover {
|
|
||||||
color: white;
|
|
||||||
background: #08f;
|
|
||||||
}
|
|
||||||
.code {
|
.code {
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
width: 40rem;
|
width: 40rem;
|
||||||
}
|
}
|
||||||
.CodeMirror {
|
.resize-grip-enabled .CodeMirror-scroll {
|
||||||
border: solid #CCC 1px;
|
|
||||||
}
|
|
||||||
.CodeMirror-scroll {
|
|
||||||
height: auto !important;;
|
height: auto !important;;
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -193,34 +190,15 @@ h2 .svg-icon, label .svg-icon {
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 6px; /* resize-grip height */
|
bottom: 6px; /* resize-grip height */
|
||||||
}
|
}
|
||||||
.CodeMirror-lint-mark-warning {
|
.resize-grip-enabled .CodeMirror-vscrollbar {
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
.CodeMirror-vscrollbar {
|
|
||||||
margin-bottom: 7px; /* make space for resize-grip */
|
margin-bottom: 7px; /* make space for resize-grip */
|
||||||
}
|
}
|
||||||
.CodeMirror-hscrollbar {
|
.resize-grip-enabled .CodeMirror-hscrollbar {
|
||||||
bottom: 7px; /* make space for resize-grip */
|
bottom: 7px; /* make space for resize-grip */
|
||||||
}
|
}
|
||||||
.CodeMirror-scrollbar-filler {
|
.resize-grip-enabled .CodeMirror-scrollbar-filler {
|
||||||
bottom: 7px; /* make space for resize-grip */
|
bottom: 7px; /* make space for resize-grip */
|
||||||
}
|
}
|
||||||
.CodeMirror-dialog {
|
|
||||||
-webkit-animation: highlight 3s ease-out;
|
|
||||||
}
|
|
||||||
.CodeMirror-focused {
|
|
||||||
outline: -webkit-focus-ring-color auto 5px;
|
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
|
||||||
.CodeMirror-search-field {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
.CodeMirror-jump-field {
|
|
||||||
width: 5em;
|
|
||||||
}
|
|
||||||
.CodeMirror-search-hint {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight,
|
body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||||
body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar {
|
body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar {
|
||||||
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
||||||
|
@ -536,6 +514,40 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************ single editor **************/
|
||||||
|
#sections .single-editor {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-editor .CodeMirror {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/************ line widget *************/
|
||||||
|
.CodeMirror-linewidget .applies-to {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 1em;
|
||||||
|
padding-right: calc(1em + 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget .applies-to li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget .applies-to li + li {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget .applies-to li:not([data-type="regexp"]) .applies-to-regexp-test {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget li.applies-to-everything {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
/************ reponsive layouts ************/
|
/************ reponsive layouts ************/
|
||||||
@media(max-width:737px) {
|
@media(max-width:737px) {
|
||||||
#header {
|
#header {
|
||||||
|
|
631
edit/edit.js
631
edit/edit.js
|
@ -1,8 +1,10 @@
|
||||||
/* eslint brace-style: 0, operator-linebreak: 0 */
|
/* eslint brace-style: 0, operator-linebreak: 0 */
|
||||||
/* global CodeMirror parserlib */
|
/* global CodeMirror parserlib */
|
||||||
/* global onDOMscripted */
|
/* global loadScript */
|
||||||
/* global css_beautify */
|
/* global css_beautify */
|
||||||
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
|
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
|
||||||
|
/* global mozParser createSourceEditor */
|
||||||
|
/* global closeCurrentTab regExpTester messageBox */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let styleId = null;
|
let styleId = null;
|
||||||
|
@ -18,6 +20,8 @@ let useHistoryBack;
|
||||||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||||
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
|
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
|
||||||
|
|
||||||
|
let editor;
|
||||||
|
|
||||||
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
|
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
|
||||||
onBackgroundReady();
|
onBackgroundReady();
|
||||||
|
|
||||||
|
@ -160,111 +164,16 @@ function setCleanSection(section) {
|
||||||
|
|
||||||
function initCodeMirror() {
|
function initCodeMirror() {
|
||||||
const CM = CodeMirror;
|
const CM = CodeMirror;
|
||||||
const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0;
|
|
||||||
// lint.js is not loaded initially
|
// lint.js is not loaded initially
|
||||||
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
CM.defaults.lint = linterConfig.getForCodeMirror();
|
||||||
if (!prefs.get('editor.keyMap')) {
|
|
||||||
prefs.reset('editor.keyMap');
|
|
||||||
}
|
|
||||||
|
|
||||||
// default option values
|
|
||||||
Object.assign(CM.defaults, {
|
|
||||||
mode: 'css',
|
|
||||||
lineNumbers: true,
|
|
||||||
lineWrapping: true,
|
|
||||||
foldGutter: true,
|
|
||||||
gutters: [
|
|
||||||
'CodeMirror-linenumbers',
|
|
||||||
'CodeMirror-foldgutter',
|
|
||||||
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
|
||||||
],
|
|
||||||
matchBrackets: true,
|
|
||||||
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
|
|
||||||
hintOptions: {},
|
|
||||||
lint: linterConfig.getForCodeMirror(),
|
|
||||||
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
|
||||||
styleActiveLine: true,
|
|
||||||
theme: 'default',
|
|
||||||
keyMap: prefs.get('editor.keyMap'),
|
|
||||||
extraKeys: {
|
|
||||||
// independent of current keyMap
|
|
||||||
'Alt-Enter': 'toggleStyle',
|
|
||||||
'Alt-PageDown': 'nextEditor',
|
|
||||||
'Alt-PageUp': 'prevEditor'
|
|
||||||
}
|
|
||||||
}, prefs.get('editor.options'));
|
|
||||||
|
|
||||||
// additional commands
|
// additional commands
|
||||||
CM.commands.jumpToLine = jumpToLine;
|
CM.commands.jumpToLine = jumpToLine;
|
||||||
CM.commands.nextEditor = cm => nextPrevEditor(cm, 1);
|
CM.commands.nextEditor = cm => nextPrevEditor(cm, 1);
|
||||||
CM.commands.prevEditor = cm => nextPrevEditor(cm, -1);
|
CM.commands.prevEditor = cm => nextPrevEditor(cm, -1);
|
||||||
CM.commands.save = save;
|
CM.commands.save = save;
|
||||||
CM.commands.blockComment = cm => {
|
|
||||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
|
||||||
};
|
|
||||||
CM.commands.toggleStyle = toggleStyle;
|
CM.commands.toggleStyle = toggleStyle;
|
||||||
|
|
||||||
// 'basic' keymap only has basic keys by design, so we skip it
|
|
||||||
|
|
||||||
const extraKeysCommands = {};
|
|
||||||
Object.keys(CM.defaults.extraKeys).forEach(key => {
|
|
||||||
extraKeysCommands[CM.defaults.extraKeys[key]] = true;
|
|
||||||
});
|
|
||||||
if (!extraKeysCommands.jumpToLine) {
|
|
||||||
CM.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
|
|
||||||
CM.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
|
|
||||||
CM.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
|
|
||||||
CM.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
|
|
||||||
}
|
|
||||||
if (!extraKeysCommands.autocomplete) {
|
|
||||||
// will be used by 'sublime' on PC via fallthrough
|
|
||||||
CM.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete';
|
|
||||||
// OSX uses Ctrl-Space and Cmd-Space for something else
|
|
||||||
CM.keyMap.macDefault['Alt-Space'] = 'autocomplete';
|
|
||||||
// copied from 'emacs' keymap
|
|
||||||
CM.keyMap.emacsy['Alt-/'] = 'autocomplete';
|
|
||||||
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
|
||||||
}
|
|
||||||
if (!extraKeysCommands.blockComment) {
|
|
||||||
CM.keyMap.sublime['Shift-Ctrl-/'] = 'blockComment';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWindowsOS) {
|
|
||||||
// 'pcDefault' keymap on Windows should have F3/Shift-F3
|
|
||||||
if (!extraKeysCommands.findNext) {
|
|
||||||
CM.keyMap.pcDefault['F3'] = 'findNext';
|
|
||||||
}
|
|
||||||
if (!extraKeysCommands.findPrev) {
|
|
||||||
CM.keyMap.pcDefault['Shift-F3'] = 'findPrev';
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
|
|
||||||
['N', 'T', 'W'].forEach(char => {
|
|
||||||
[
|
|
||||||
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
|
||||||
// Note: modifier order in CM is S-C-A
|
|
||||||
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']}
|
|
||||||
].forEach(remap => {
|
|
||||||
const oldKey = remap.from + char;
|
|
||||||
Object.keys(CM.keyMap).forEach(keyMapName => {
|
|
||||||
const keyMap = CM.keyMap[keyMapName];
|
|
||||||
const command = keyMap[oldKey];
|
|
||||||
if (!command) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
remap.to.some(newMod => {
|
|
||||||
const newKey = newMod + char;
|
|
||||||
if (!(newKey in keyMap)) {
|
|
||||||
delete keyMap[oldKey];
|
|
||||||
keyMap[newKey] = command;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// user option values
|
// user option values
|
||||||
CM.getOption = o => CodeMirror.defaults[o];
|
CM.getOption = o => CodeMirror.defaults[o];
|
||||||
CM.setOption = (o, v) => {
|
CM.setOption = (o, v) => {
|
||||||
|
@ -434,11 +343,7 @@ function acmeEventListener(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'autocompleteOnTyping':
|
case 'autocompleteOnTyping':
|
||||||
editors.forEach(cm => {
|
editors.forEach(cm => setupAutocomplete(cm, el.checked));
|
||||||
const onOff = el.checked ? 'on' : 'off';
|
|
||||||
cm[onOff]('changes', autocompleteOnTyping);
|
|
||||||
cm[onOff]('pick', autocompletePicked);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
case 'matchHighlight':
|
case 'matchHighlight':
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
@ -463,8 +368,7 @@ function setupCodeMirror(textarea, index) {
|
||||||
|
|
||||||
cm.on('changes', indicateCodeChangeDebounced);
|
cm.on('changes', indicateCodeChangeDebounced);
|
||||||
if (prefs.get('editor.autocompleteOnTyping')) {
|
if (prefs.get('editor.autocompleteOnTyping')) {
|
||||||
cm.on('changes', autocompleteOnTyping);
|
setupAutocomplete(cm);
|
||||||
cm.on('pick', autocompletePicked);
|
|
||||||
}
|
}
|
||||||
wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
|
wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
|
||||||
cm.on('blur', () => {
|
cm.on('blur', () => {
|
||||||
|
@ -504,6 +408,7 @@ function setupCodeMirror(textarea, index) {
|
||||||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wrapper.classList.add('resize-grip-enabled');
|
||||||
let lastClickTime = 0;
|
let lastClickTime = 0;
|
||||||
const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
|
const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
|
||||||
resizeGrip.onmousedown = event => {
|
resizeGrip.onmousedown = event => {
|
||||||
|
@ -671,12 +576,20 @@ window.onbeforeunload = () => {
|
||||||
rememberWindowSize();
|
rememberWindowSize();
|
||||||
}
|
}
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
if (isCleanGlobal()) {
|
if (isClean()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateLintReportIfEnabled(null, 0);
|
updateLintReportIfEnabled(null, 0);
|
||||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||||
return t('styleChangesNotSaved');
|
return t('styleChangesNotSaved');
|
||||||
|
|
||||||
|
function isClean() {
|
||||||
|
if (editor) {
|
||||||
|
return !editor.isDirty();
|
||||||
|
} else {
|
||||||
|
return isCleanGlobal();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function addAppliesTo(list, name, value) {
|
function addAppliesTo(list, name, value) {
|
||||||
|
@ -737,20 +650,30 @@ function addSection(event, section) {
|
||||||
|
|
||||||
toggleTestRegExpVisibility();
|
toggleTestRegExpVisibility();
|
||||||
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
|
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
|
||||||
$('.test-regexp', div).onclick = showRegExpTester;
|
$('.test-regexp', div).onclick = () => {
|
||||||
function toggleTestRegExpVisibility() {
|
regExpTester.toggle();
|
||||||
const show = [...appliesTo.children].some(item =>
|
regExpTester.update(getRegExps());
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRegExps() {
|
||||||
|
return [...appliesTo.children]
|
||||||
|
.map(item =>
|
||||||
!item.matches('.applies-to-everything') &&
|
!item.matches('.applies-to-everything') &&
|
||||||
$('.applies-type', item).value === 'regexp' &&
|
$('.applies-type', item).value === 'regexp' &&
|
||||||
$('.applies-value', item).value.trim()
|
$('.applies-value', item).value.trim()
|
||||||
);
|
)
|
||||||
|
.filter(item => item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTestRegExpVisibility() {
|
||||||
|
const show = getRegExps().length > 0;
|
||||||
div.classList.toggle('has-regexp', show);
|
div.classList.toggle('has-regexp', show);
|
||||||
appliesTo.oninput = appliesTo.oninput || show && (event => {
|
appliesTo.oninput = appliesTo.oninput || show && (event => {
|
||||||
if (
|
if (
|
||||||
event.target.matches('.applies-value') &&
|
event.target.matches('.applies-value') &&
|
||||||
$('.applies-type', event.target.parentElement).value === 'regexp'
|
$('.applies-type', event.target.parentElement).value === 'regexp'
|
||||||
) {
|
) {
|
||||||
showRegExpTester(null, div);
|
regExpTester.update(getRegExps());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1075,6 +998,14 @@ function jumpToLine(cm) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleStyle() {
|
function toggleStyle() {
|
||||||
|
if (editor) {
|
||||||
|
editor.toggleStyle();
|
||||||
|
} else {
|
||||||
|
toggleSectionStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSectionStyle() {
|
||||||
$('#enabled').checked = !$('#enabled').checked;
|
$('#enabled').checked = !$('#enabled').checked;
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
@ -1100,6 +1031,12 @@ function toggleSectionHeight(cm) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupAutocomplete(cm, enable = true) {
|
||||||
|
const onOff = enable ? 'on' : 'off';
|
||||||
|
cm[onOff]('changes', autocompleteOnTyping);
|
||||||
|
cm[onOff]('pick', autocompletePicked);
|
||||||
|
}
|
||||||
|
|
||||||
function autocompleteOnTyping(cm, [info], debounced) {
|
function autocompleteOnTyping(cm, [info], debounced) {
|
||||||
if (
|
if (
|
||||||
cm.state.completionActive ||
|
cm.state.completionActive ||
|
||||||
|
@ -1266,14 +1203,13 @@ function getEditorInSight(nearbyElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function beautify(event) {
|
function beautify(event) {
|
||||||
onDOMscripted([
|
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
||||||
'vendor-overwrites/beautify/beautify-css-mod.js',
|
.then(() => {
|
||||||
() => {
|
|
||||||
if (!window.css_beautify && window.exports) {
|
if (!window.css_beautify && window.exports) {
|
||||||
window.css_beautify = window.exports.css_beautify;
|
window.css_beautify = window.exports.css_beautify;
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
]).then(doBeautify);
|
.then(doBeautify);
|
||||||
|
|
||||||
function doBeautify() {
|
function doBeautify() {
|
||||||
const tabs = prefs.get('editor.indentWithTabs');
|
const tabs = prefs.get('editor.indentWithTabs');
|
||||||
|
@ -1361,46 +1297,52 @@ onDOMready().then(init);
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
initCodeMirror();
|
initCodeMirror();
|
||||||
const params = getParams();
|
getStyle().then(style => {
|
||||||
if (!params.id) {
|
styleId = style.id;
|
||||||
|
sessionStorage.justEditedStyleId = styleId;
|
||||||
|
|
||||||
|
if (!isUsercss(style)) {
|
||||||
|
initWithSectionStyle({style});
|
||||||
|
} else {
|
||||||
|
editor = createSourceEditor(style);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getStyle() {
|
||||||
|
const id = new URLSearchParams(location.search).get('id');
|
||||||
|
if (!id) {
|
||||||
// match should be 2 - one for the whole thing, one for the parentheses
|
// match should be 2 - one for the whole thing, one for the parentheses
|
||||||
// This is an add
|
// This is an add
|
||||||
$('#heading').textContent = t('addStyleTitle');
|
$('#heading').textContent = t('addStyleTitle');
|
||||||
const section = {code: ''};
|
return Promise.resolve(createEmptyStyle());
|
||||||
for (const i in CssToProperty) {
|
|
||||||
if (params[i]) {
|
|
||||||
section[CssToProperty[i]] = [params[i]];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
addSection(null, section);
|
|
||||||
editors[0].setOption('lint', CodeMirror.defaults.lint);
|
|
||||||
editors[0].focus();
|
|
||||||
// default to enabled
|
|
||||||
$('#enabled').checked = true;
|
|
||||||
initHooks();
|
|
||||||
setCleanGlobal();
|
|
||||||
updateTitle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// This is an edit
|
|
||||||
$('#heading').textContent = t('editStyleHeading');
|
$('#heading').textContent = t('editStyleHeading');
|
||||||
getStylesSafe({id: params.id}).then(styles => {
|
// This is an edit
|
||||||
|
return getStylesSafe({id}).then(styles => {
|
||||||
let style = styles[0];
|
let style = styles[0];
|
||||||
if (!style) {
|
if (!style) {
|
||||||
style = {id: null, sections: []};
|
style = createEmptyStyle();
|
||||||
history.replaceState({}, document.title, location.pathname);
|
history.replaceState({}, document.title, location.pathname);
|
||||||
}
|
}
|
||||||
styleId = style.id;
|
return style;
|
||||||
sessionStorage.justEditedStyleId = styleId;
|
|
||||||
setStyleMeta(style);
|
|
||||||
window.onload = () => {
|
|
||||||
window.onload = null;
|
|
||||||
initWithStyle({style});
|
|
||||||
};
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
window.onload();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyStyle() {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const style = {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
enabled: true,
|
||||||
|
sections: [{code: ''}]
|
||||||
|
};
|
||||||
|
for (const i in CssToProperty) {
|
||||||
|
if (params.get(i)) {
|
||||||
|
style.sections[0][CssToProperty[i]] = [params.get(i)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStyleMeta(style) {
|
function setStyleMeta(style) {
|
||||||
|
@ -1409,7 +1351,14 @@ function setStyleMeta(style) {
|
||||||
$('#url').href = style.url || '';
|
$('#url').href = style.url || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function initWithStyle({style, codeIsUpdated}) {
|
function isUsercss(style) {
|
||||||
|
return (
|
||||||
|
style.usercssData ||
|
||||||
|
!style.id && prefs.get('newStyleAsUsercss')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWithSectionStyle({style, codeIsUpdated}) {
|
||||||
setStyleMeta(style);
|
setStyleMeta(style);
|
||||||
|
|
||||||
if (codeIsUpdated === false) {
|
if (codeIsUpdated === false) {
|
||||||
|
@ -1452,6 +1401,16 @@ function initWithStyle({style, codeIsUpdated}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupOptionsExpand() {
|
||||||
|
$('#options').open = prefs.get('editor.options.expanded');
|
||||||
|
$('#options h2').addEventListener('click', () => {
|
||||||
|
setTimeout(() => prefs.set('editor.options.expanded', $('#options').open));
|
||||||
|
});
|
||||||
|
prefs.subscribe(['editor.options.expanded'], (key, value) => {
|
||||||
|
$('#options').open = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initHooks() {
|
function initHooks() {
|
||||||
if (initHooks.alreadyDone) {
|
if (initHooks.alreadyDone) {
|
||||||
return;
|
return;
|
||||||
|
@ -1471,14 +1430,7 @@ function initHooks() {
|
||||||
$('#keyMap-help').addEventListener('click', showKeyMapHelp, false);
|
$('#keyMap-help').addEventListener('click', showKeyMapHelp, false);
|
||||||
$('#cancel-button').addEventListener('click', goBackToManage);
|
$('#cancel-button').addEventListener('click', goBackToManage);
|
||||||
|
|
||||||
$('#options').open = prefs.get('editor.options.expanded');
|
setupOptionsExpand();
|
||||||
$('#options h2').addEventListener('click', () => {
|
|
||||||
setTimeout(() => prefs.set('editor.options.expanded', $('#options').open));
|
|
||||||
});
|
|
||||||
prefs.subscribe(['editor.options.expanded'], (key, value) => {
|
|
||||||
$('#options').open = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
initLint();
|
initLint();
|
||||||
|
|
||||||
if (!FIREFOX) {
|
if (!FIREFOX) {
|
||||||
|
@ -1605,6 +1557,14 @@ function updateLintReportIfEnabled(...args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
if (editor) {
|
||||||
|
editor.save();
|
||||||
|
} else {
|
||||||
|
saveSectionStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSectionStyle() {
|
||||||
updateLintReportIfEnabled(null, 0);
|
updateLintReportIfEnabled(null, 0);
|
||||||
|
|
||||||
// save the contents of the CodeMirror editors back into the textareas
|
// save the contents of the CodeMirror editors back into the textareas
|
||||||
|
@ -1679,17 +1639,7 @@ function showMozillaFormat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMozillaFormat() {
|
function toMozillaFormat() {
|
||||||
return getSectionsHashes().map(section => {
|
return mozParser.format({sections: getSectionsHashes()});
|
||||||
let cssMds = [];
|
|
||||||
for (const i in propertyToCss) {
|
|
||||||
if (section[i]) {
|
|
||||||
cssMds = cssMds.concat(section[i].map(v =>
|
|
||||||
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
|
|
||||||
}).join('\n\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromMozillaFormat() {
|
function fromMozillaFormat() {
|
||||||
|
@ -1714,133 +1664,29 @@ function fromMozillaFormat() {
|
||||||
});
|
});
|
||||||
|
|
||||||
function doImport(event) {
|
function doImport(event) {
|
||||||
// parserlib contained in CSSLint-worker.js
|
const replaceOldStyle = event.target.name === 'import-replace';
|
||||||
onDOMscripted(['vendor-overwrites/csslint/csslint-worker.js']).then(() => {
|
const mozStyle = trimNewLines(popup.codebox.getValue());
|
||||||
doImportWhenReady(event.target);
|
|
||||||
|
mozParser.parse(mozStyle)
|
||||||
|
.then(updateSection)
|
||||||
|
.then(() => {
|
||||||
editors.forEach(cm => updateLintReportIfEnabled(cm, 1));
|
editors.forEach(cm => updateLintReportIfEnabled(cm, 1));
|
||||||
editors.last.state.renderLintReportNow = true;
|
editors.last.state.renderLintReportNow = true;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function doImportWhenReady(target) {
|
|
||||||
const replaceOldStyle = target.name === 'import-replace';
|
|
||||||
$('.dismiss', popup).onclick();
|
$('.dismiss', popup).onclick();
|
||||||
const mozStyle = trimNewLines(popup.codebox.getValue());
|
})
|
||||||
const parser = new parserlib.css.Parser();
|
.catch(showError);
|
||||||
const lines = mozStyle.split('\n');
|
|
||||||
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
|
|
||||||
const errors = [];
|
|
||||||
// let oldSectionCount = editors.length;
|
|
||||||
let firstAddedCM;
|
|
||||||
|
|
||||||
parser.addListener('startdocument', function (e) {
|
function showError(errors) {
|
||||||
let outerText = getRange(sectionStack.last.start, (--e.col, e));
|
if (!Array.isArray(errors)) {
|
||||||
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
|
errors = [errors];
|
||||||
const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')};
|
|
||||||
// move last comment before @-moz-document inside the section
|
|
||||||
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
|
|
||||||
section.code = gapComment[1] + '\n';
|
|
||||||
outerText = trimNewLines(outerText.substring(0, gapComment.index));
|
|
||||||
}
|
}
|
||||||
if (outerText.trim()) {
|
showHelp(t('styleFromMozillaFormatError'), $element({
|
||||||
sectionStack.last.code = outerText;
|
|
||||||
doAddSection(sectionStack.last);
|
|
||||||
sectionStack.last.code = '';
|
|
||||||
}
|
|
||||||
for (const f of e.functions) {
|
|
||||||
const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/);
|
|
||||||
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
|
|
||||||
errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const aType = CssToProperty[m[1]];
|
|
||||||
const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\');
|
|
||||||
(section[aType] = section[aType] || []).push(aValue);
|
|
||||||
}
|
|
||||||
sectionStack.push(section);
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.addListener('enddocument', function () {
|
|
||||||
const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start');
|
|
||||||
const section = sectionStack.pop();
|
|
||||||
section.code += getRange(section.start, end);
|
|
||||||
sectionStack.last.start = (++end.col, end);
|
|
||||||
doAddSection(section);
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.addListener('endstylesheet', () => {
|
|
||||||
// add nonclosed outer sections (either broken or the last global one)
|
|
||||||
const endOfText = {line: lines.length, col: lines.last.length + 1};
|
|
||||||
sectionStack.last.code += getRange(sectionStack.last.start, endOfText);
|
|
||||||
sectionStack.forEach(doAddSection);
|
|
||||||
|
|
||||||
delete maximizeCodeHeight.stats;
|
|
||||||
editors.forEach(cm => {
|
|
||||||
maximizeCodeHeight(cm.getSection(), cm === editors.last);
|
|
||||||
});
|
|
||||||
|
|
||||||
makeSectionVisible(firstAddedCM);
|
|
||||||
firstAddedCM.focus();
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
showHelp(t('linterIssues'), $element({
|
|
||||||
tag: 'pre',
|
tag: 'pre',
|
||||||
textContent: errors.join('\n'),
|
textContent: errors.join('\n'),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
parser.addListener('error', e => {
|
function updateSection(sections) {
|
||||||
errors.push(e.line + ':' + e.col + ' ' +
|
|
||||||
e.message.replace(/ at line \d.+$/, ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.parse(mozStyle);
|
|
||||||
|
|
||||||
function getRange(start, end) {
|
|
||||||
const L1 = start.line - 1;
|
|
||||||
const C1 = start.col - 1;
|
|
||||||
const L2 = end.line - 1;
|
|
||||||
const C2 = end.col - 1;
|
|
||||||
if (L1 === L2) {
|
|
||||||
return lines[L1].substr(C1, C2 - C1 + 1);
|
|
||||||
} else {
|
|
||||||
const middle = lines.slice(L1 + 1, L2).join('\n');
|
|
||||||
return lines[L1].substr(C1) + '\n' + middle +
|
|
||||||
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function doAddSection(section) {
|
|
||||||
section.code = section.code.trim();
|
|
||||||
// don't add empty sections
|
|
||||||
if (
|
|
||||||
!section.code &&
|
|
||||||
!section.urls &&
|
|
||||||
!section.urlPrefixes &&
|
|
||||||
!section.domains &&
|
|
||||||
!section.regexps
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!firstAddedCM) {
|
|
||||||
if (!initFirstSection(section)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCleanItem(addSection(null, section), false);
|
|
||||||
firstAddedCM = firstAddedCM || editors.last;
|
|
||||||
}
|
|
||||||
// do onetime housekeeping as the imported text is confirmed to be a valid style
|
|
||||||
function initFirstSection(section) {
|
|
||||||
// skip adding the first global section when there's no code/comments
|
|
||||||
if (
|
|
||||||
/* ignore boilerplate NS */
|
|
||||||
!section.code.replace('@namespace url(http://www.w3.org/1999/xhtml);', '')
|
|
||||||
/* ignore all whitespace including new lines */
|
|
||||||
.replace(/[\s\n]/g, '')
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (replaceOldStyle) {
|
if (replaceOldStyle) {
|
||||||
editors.slice(0).reverse().forEach(cm => {
|
editors.slice(0).reverse().forEach(cm => {
|
||||||
removeSection({target: cm.getSection().firstElementChild});
|
removeSection({target: cm.getSection().firstElementChild});
|
||||||
|
@ -1851,17 +1697,24 @@ function fromMozillaFormat() {
|
||||||
removeSection({target: editors.last.getSection()});
|
removeSection({target: editors.last.getSection()});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
const firstSection = sections[0];
|
||||||
}
|
setCleanItem(addSection(null, firstSection), false);
|
||||||
function backtrackTo(parser, tokenType, startEnd) {
|
const firstAddedCM = editors.last;
|
||||||
const tokens = parser._tokenStream._lt;
|
for (const section of sections.slice(1)) {
|
||||||
for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) {
|
setCleanItem(addSection(null, section), false);
|
||||||
if (tokens[i].type === tokenType) {
|
}
|
||||||
return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']};
|
|
||||||
}
|
delete maximizeCodeHeight.stats;
|
||||||
|
editors.forEach(cm => {
|
||||||
|
maximizeCodeHeight(cm.getSection(), cm === editors.last);
|
||||||
|
});
|
||||||
|
|
||||||
|
makeSectionVisible(firstAddedCM);
|
||||||
|
firstAddedCM.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimNewLines(s) {
|
function trimNewLines(s) {
|
||||||
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
||||||
}
|
}
|
||||||
|
@ -1984,151 +1837,6 @@ function showKeyMapHelp() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = [...$('.applies-to-list', section).children]
|
|
||||||
.map(item =>
|
|
||||||
!item.matches('.applies-to-everything') &&
|
|
||||||
$('.applies-type', item).value === 'regexp' &&
|
|
||||||
$('.applies-value', item).value.trim()
|
|
||||||
)
|
|
||||||
.filter(item => item)
|
|
||||||
.map(text => {
|
|
||||||
const rxData = Object.assign({text}, cachedRegexps.get(text));
|
|
||||||
if (!rxData.urls) {
|
|
||||||
cachedRegexps.set(text, Object.assign(rxData, {
|
|
||||||
// imitate buggy Stylish-for-chrome, see detectSloppyRegexps()
|
|
||||||
rx: tryRegExp('^' + text + '$'),
|
|
||||||
urls: new Map(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return rxData;
|
|
||||||
});
|
|
||||||
chrome.tabs.onUpdated.addListener(function _(tabId, info) {
|
|
||||||
if ($('.regexp-report')) {
|
|
||||||
if (info.url) {
|
|
||||||
showRegExpTester(event, section);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chrome.tabs.onUpdated.removeListener(_);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const getMatchInfo = m => m && {text: m[0], pos: m.index};
|
|
||||||
|
|
||||||
queryTabs().then(tabs => {
|
|
||||||
const supported = tabs.map(tab => tab.url)
|
|
||||||
.filter(url => URLS.supported(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) || getMatchInfo(url.match(rx));
|
|
||||||
if (match) {
|
|
||||||
urlsNow.set(url, match);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rxData.urls = urlsNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const stats = {
|
|
||||||
full: {data: [], label: t('styleRegexpTestFull')},
|
|
||||||
partial: {data: [], label: [
|
|
||||||
t('styleRegexpTestPartial'),
|
|
||||||
template.regexpTestPartial.cloneNode(true),
|
|
||||||
]},
|
|
||||||
none: {data: [], label: t('styleRegexpTestNone')},
|
|
||||||
invalid: {data: [], label: t('styleRegexpTestInvalid')},
|
|
||||||
};
|
|
||||||
// collect stats
|
|
||||||
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 = $element({tag: 'img', src: faviconUrl});
|
|
||||||
if (match.text.length === url.length) {
|
|
||||||
full.push($element({appendChild: [
|
|
||||||
icon,
|
|
||||||
url,
|
|
||||||
]}));
|
|
||||||
} else {
|
|
||||||
partial.push($element({appendChild: [
|
|
||||||
icon,
|
|
||||||
url.substr(0, match.pos),
|
|
||||||
$element({tag: 'mark', textContent: match.text}),
|
|
||||||
url.substr(match.pos + match.text.length),
|
|
||||||
]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (full.length) {
|
|
||||||
stats.full.data.push({text, urls: full});
|
|
||||||
}
|
|
||||||
if (partial.length) {
|
|
||||||
stats.partial.data.push({text, urls: partial});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// render stats
|
|
||||||
const report = $element({className: 'regexp-report'});
|
|
||||||
const br = $element({tag: 'br'});
|
|
||||||
for (const type in stats) {
|
|
||||||
// top level groups: full, partial, none, invalid
|
|
||||||
const {label, data} = stats[type];
|
|
||||||
if (!data.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const block = report.appendChild($element({
|
|
||||||
tag: 'details',
|
|
||||||
open: true,
|
|
||||||
dataset: {type},
|
|
||||||
appendChild: $element({tag: 'summary', appendChild: label}),
|
|
||||||
}));
|
|
||||||
// 2nd level: regexp text
|
|
||||||
for (const {text, urls} of data) {
|
|
||||||
if (urls) {
|
|
||||||
// type is partial or full
|
|
||||||
block.appendChild($element({
|
|
||||||
tag: 'details',
|
|
||||||
open: true,
|
|
||||||
appendChild: [
|
|
||||||
$element({tag: 'summary', textContent: text}),
|
|
||||||
// 3rd level: tab urls
|
|
||||||
...urls,
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// type is none or invalid
|
|
||||||
block.appendChild(document.createTextNode(text));
|
|
||||||
block.appendChild(br.cloneNode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showHelp(t('styleRegexpTestTitle'), report);
|
|
||||||
|
|
||||||
$('.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, body) {
|
function showHelp(title, body) {
|
||||||
const div = $('#help-popup');
|
const div = $('#help-popup');
|
||||||
div.classList.remove('big');
|
div.classList.remove('big');
|
||||||
|
@ -2182,40 +1890,55 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
return popup;
|
return popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getParams() {
|
|
||||||
const params = {};
|
|
||||||
const urlParts = location.href.split('?', 2);
|
|
||||||
if (urlParts.length === 1) {
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
urlParts[1].split('&').forEach(keyValue => {
|
|
||||||
const splitKeyValue = keyValue.split('=', 2);
|
|
||||||
params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]);
|
|
||||||
});
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||||
|
|
||||||
|
function replaceStyle(request) {
|
||||||
|
const codeIsUpdated = request.codeIsUpdated !== false;
|
||||||
|
if (!isUsercss(request.style)) {
|
||||||
|
initWithSectionStyle(request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!codeIsUpdated) {
|
||||||
|
editor.replaceMeta(request.style);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
askDiscardChanges()
|
||||||
|
.then(result => {
|
||||||
|
if (result) {
|
||||||
|
editor.replaceStyle(request.style);
|
||||||
|
} else {
|
||||||
|
editor.setStyleDirty(request.style);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function askDiscardChanges() {
|
||||||
|
if (!editor.isTouched()) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
return messageBox.confirm(t('styleUpdateDiscardChanges'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onRuntimeMessage(request) {
|
function onRuntimeMessage(request) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (styleId && styleId === request.style.id && request.reason !== 'editSave') {
|
if (styleId && styleId === request.style.id && request.reason !== 'editSave' && request.reason !== 'config') {
|
||||||
if ((request.style.sections[0] || {}).code === null) {
|
if ((request.style.sections[0] || {}).code === null) {
|
||||||
// the code-less style came from notifyAllTabs
|
// the code-less style came from notifyAllTabs
|
||||||
onBackgroundReady().then(() => {
|
onBackgroundReady().then(() => {
|
||||||
request.style = BG.cachedStyles.byId.get(request.style.id);
|
request.style = BG.cachedStyles.byId.get(request.style.id);
|
||||||
initWithStyle(request);
|
replaceStyle(request);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
initWithStyle(request);
|
replaceStyle(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
if (styleId && styleId === request.id) {
|
if (styleId === request.id || editor && editor.getStyle().id === request.id) {
|
||||||
window.onbeforeunload = () => {};
|
window.onbeforeunload = () => {};
|
||||||
window.close();
|
closeCurrentTab();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -27,7 +27,7 @@ window.linterConfig.defaults.stylelint = (defaultSeverity => ({
|
||||||
'property-no-unknown': [true, defaultSeverity],
|
'property-no-unknown': [true, defaultSeverity],
|
||||||
'selector-pseudo-class-no-unknown': [true, defaultSeverity],
|
'selector-pseudo-class-no-unknown': [true, defaultSeverity],
|
||||||
'selector-pseudo-element-no-unknown': [true, defaultSeverity],
|
'selector-pseudo-element-no-unknown': [true, defaultSeverity],
|
||||||
'selector-type-no-unknown': [true, defaultSeverity],
|
'selector-type-no-unknown': false, // for scss/less/stylus-lang
|
||||||
'string-no-newline': [true, defaultSeverity],
|
'string-no-newline': [true, defaultSeverity],
|
||||||
'unit-no-unknown': [true, defaultSeverity],
|
'unit-no-unknown': [true, defaultSeverity],
|
||||||
|
|
||||||
|
|
123
edit/lint.js
123
edit/lint.js
|
@ -1,9 +1,10 @@
|
||||||
/* global CodeMirror messageBox */
|
/* global CodeMirror messageBox */
|
||||||
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
|
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
|
||||||
/* global onDOMscripted injectCSS require CSSLint stylelint */
|
/* global loadScript require CSSLint stylelint */
|
||||||
|
/* global makeLink */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
loadLinterAssets();
|
onDOMready().then(loadLinterAssets);
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var linterConfig = {
|
var linterConfig = {
|
||||||
|
@ -20,18 +21,27 @@ var linterConfig = {
|
||||||
stylelint: 'editorStylelintConfig',
|
stylelint: 'editorStylelintConfig',
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrent(linter = prefs.get('editor.linter')) {
|
getDefault() {
|
||||||
|
// some dirty hacks to override editor.linter getting from prefs
|
||||||
|
const linter = prefs.get('editor.linter');
|
||||||
|
if (linter && editors[0] && editors[0].getOption('mode') !== 'css') {
|
||||||
|
return 'stylelint';
|
||||||
|
}
|
||||||
|
return linter;
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrent(linter = linterConfig.getDefault()) {
|
||||||
return this.fallbackToDefaults(this[linter] || {});
|
return this.fallbackToDefaults(this[linter] || {});
|
||||||
},
|
},
|
||||||
|
|
||||||
getForCodeMirror(linter = prefs.get('editor.linter')) {
|
getForCodeMirror(linter = linterConfig.getDefault()) {
|
||||||
return CodeMirror.lint && CodeMirror.lint[linter] ? {
|
return CodeMirror.lint && CodeMirror.lint[linter] ? {
|
||||||
getAnnotations: CodeMirror.lint[linter],
|
getAnnotations: CodeMirror.lint[linter],
|
||||||
delay: prefs.get('editor.lintDelay'),
|
delay: prefs.get('editor.lintDelay'),
|
||||||
} : false;
|
} : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
fallbackToDefaults(config, linter = prefs.get('editor.linter')) {
|
fallbackToDefaults(config, linter = linterConfig.getDefault()) {
|
||||||
if (config && Object.keys(config).length) {
|
if (config && Object.keys(config).length) {
|
||||||
if (linter === 'stylelint') {
|
if (linter === 'stylelint') {
|
||||||
// always use default syntax because we don't expose it in config UI
|
// always use default syntax because we don't expose it in config UI
|
||||||
|
@ -43,16 +53,16 @@ var linterConfig = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setLinter(linter = prefs.get('editor.linter')) {
|
setLinter(linter = linterConfig.getDefault()) {
|
||||||
linter = linter.toLowerCase();
|
linter = linter.toLowerCase();
|
||||||
linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
|
linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
|
||||||
if (prefs.get('editor.linter') !== linter) {
|
if (linterConfig.getDefault() !== linter) {
|
||||||
prefs.set('editor.linter', linter);
|
prefs.set('editor.linter', linter);
|
||||||
}
|
}
|
||||||
return linter;
|
return linter;
|
||||||
},
|
},
|
||||||
|
|
||||||
findInvalidRules(config, linter = prefs.get('editor.linter')) {
|
findInvalidRules(config, linter = linterConfig.getDefault()) {
|
||||||
const rules = linter === 'stylelint' ? config.rules : config;
|
const rules = linter === 'stylelint' ? config.rules : config;
|
||||||
const allRules = new Set(
|
const allRules = new Set(
|
||||||
linter === 'stylelint'
|
linter === 'stylelint'
|
||||||
|
@ -63,7 +73,7 @@ var linterConfig = {
|
||||||
},
|
},
|
||||||
|
|
||||||
stringify(config = this.getCurrent()) {
|
stringify(config = this.getCurrent()) {
|
||||||
if (prefs.get('editor.linter') === 'stylelint') {
|
if (linterConfig.getDefault() === 'stylelint') {
|
||||||
config.syntax = undefined;
|
config.syntax = undefined;
|
||||||
}
|
}
|
||||||
return JSON.stringify(config, null, 2)
|
return JSON.stringify(config, null, 2)
|
||||||
|
@ -72,7 +82,7 @@ var linterConfig = {
|
||||||
|
|
||||||
save(config) {
|
save(config) {
|
||||||
config = this.fallbackToDefaults(config);
|
config = this.fallbackToDefaults(config);
|
||||||
const linter = prefs.get('editor.linter');
|
const linter = linterConfig.getDefault();
|
||||||
this[linter] = config;
|
this[linter] = config;
|
||||||
BG.chromeSync.setLZValue(this.storageName[linter], config);
|
BG.chromeSync.setLZValue(this.storageName[linter], config);
|
||||||
return config;
|
return config;
|
||||||
|
@ -117,6 +127,13 @@ var linterConfig = {
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!linterConfig.init.pending) {
|
||||||
|
linterConfig.init.pending = linterConfig.loadAll();
|
||||||
|
}
|
||||||
|
return linterConfig.init.pending;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function initLint() {
|
function initLint() {
|
||||||
|
@ -130,21 +147,22 @@ function initLint() {
|
||||||
$('#lint h2').addEventListener('click', toggleLintReport);
|
$('#lint h2').addEventListener('click', toggleLintReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
linterConfig.loadAll();
|
updateLinter();
|
||||||
linterConfig.watchStorage();
|
linterConfig.watchStorage();
|
||||||
prefs.subscribe(['editor.linter'], updateLinter);
|
prefs.subscribe(['editor.linter'], updateLinter);
|
||||||
updateLinter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLinter({immediately} = {}) {
|
function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
|
||||||
if (!immediately) {
|
if (!immediately) {
|
||||||
debounce(updateLinter, 0, {immediately: true});
|
debounce(updateLinter, 0, {immediately: true, linter});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const linter = prefs.get('editor.linter');
|
|
||||||
const GUTTERS_CLASS = 'CodeMirror-lint-markers';
|
const GUTTERS_CLASS = 'CodeMirror-lint-markers';
|
||||||
|
|
||||||
loadLinterAssets(linter).then(updateEditors);
|
Promise.all([
|
||||||
|
linterConfig.init(),
|
||||||
|
loadLinterAssets(linter)
|
||||||
|
]).then(updateEditors);
|
||||||
$('#linter-settings').style.display = !linter ? 'none' : 'inline-block';
|
$('#linter-settings').style.display = !linter ? 'none' : 'inline-block';
|
||||||
$('#lint').style.display = 'none';
|
$('#lint').style.display = 'none';
|
||||||
|
|
||||||
|
@ -357,13 +375,7 @@ function toggleLintReport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLintHelp() {
|
function showLintHelp() {
|
||||||
const makeLink = (href, textContent) => $element({
|
const linter = linterConfig.getDefault();
|
||||||
tag: 'a',
|
|
||||||
target: '_blank',
|
|
||||||
href,
|
|
||||||
textContent,
|
|
||||||
});
|
|
||||||
const linter = prefs.get('editor.linter');
|
|
||||||
const baseUrl = linter === 'stylelint'
|
const baseUrl = linter === 'stylelint'
|
||||||
? 'https://stylelint.io/user-guide/rules/'
|
? 'https://stylelint.io/user-guide/rules/'
|
||||||
// some CSSLint rules do not have a url
|
// some CSSLint rules do not have a url
|
||||||
|
@ -451,7 +463,7 @@ function setupLinterSettingsEvents(popup) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLinterPopup(config) {
|
function setupLinterPopup(config) {
|
||||||
const linter = prefs.get('editor.linter');
|
const linter = linterConfig.getDefault();
|
||||||
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
||||||
|
|
||||||
function makeButton(className, text, options = {}) {
|
function makeButton(className, text, options = {}) {
|
||||||
|
@ -503,43 +515,48 @@ function setupLinterPopup(config) {
|
||||||
$('.save', popup).disabled = cm.isClean();
|
$('.save', popup).disabled = cm.isClean();
|
||||||
});
|
});
|
||||||
setupLinterSettingsEvents(popup);
|
setupLinterSettingsEvents(popup);
|
||||||
onDOMscripted([
|
loadScript([
|
||||||
'vendor/codemirror/mode/javascript/javascript.js',
|
'/vendor/codemirror/mode/javascript/javascript.js',
|
||||||
'vendor/codemirror/addon/lint/json-lint.js',
|
'/vendor/codemirror/addon/lint/json-lint.js',
|
||||||
'vendor/jsonlint/jsonlint.js'
|
'/vendor/jsonlint/jsonlint.js'
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
popup.codebox.setOption('mode', 'application/json');
|
popup.codebox.setOption('mode', 'application/json');
|
||||||
popup.codebox.setOption('lint', 'json');
|
popup.codebox.setOption('lint', 'json');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLinterAssets(name = prefs.get('editor.linter')) {
|
function loadLinterAssets(name = linterConfig.getDefault()) {
|
||||||
if (loadLinterAssets.loadingName === name) {
|
if (!name) {
|
||||||
return onDOMscripted();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
loadLinterAssets.loadingName = name;
|
return loadLibrary().then(loadAddon);
|
||||||
const scripts = [];
|
|
||||||
|
function loadLibrary() {
|
||||||
if (name === 'csslint' && !window.CSSLint) {
|
if (name === 'csslint' && !window.CSSLint) {
|
||||||
scripts.push(
|
return loadScript([
|
||||||
'vendor-overwrites/csslint/csslint-worker.js',
|
'/vendor-overwrites/csslint/csslint-worker.js',
|
||||||
'edit/lint-defaults-csslint.js'
|
'/edit/lint-defaults-csslint.js'
|
||||||
);
|
]);
|
||||||
} else if (name === 'stylelint' && !window.stylelint) {
|
|
||||||
scripts.push(
|
|
||||||
'vendor-overwrites/stylelint/stylelint-bundle.min.js',
|
|
||||||
() => (window.stylelint = require('stylelint')),
|
|
||||||
'edit/lint-defaults-stylelint.js'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (name && !$('script[src$="vendor/codemirror/addon/lint/lint.js"]')) {
|
if (name === 'stylelint' && !window.stylelint) {
|
||||||
injectCSS('vendor/codemirror/addon/lint/lint.css');
|
return loadScript([
|
||||||
injectCSS('msgbox/msgbox.css');
|
'/vendor-overwrites/stylelint/stylelint-bundle.min.js',
|
||||||
scripts.push(
|
'/edit/lint-defaults-stylelint.js'
|
||||||
'vendor/codemirror/addon/lint/lint.js',
|
]).then(() => (window.stylelint = require('stylelint')));
|
||||||
'edit/lint-codemirror-helper.js',
|
}
|
||||||
'msgbox/msgbox.js'
|
return Promise.resolve();
|
||||||
);
|
}
|
||||||
|
|
||||||
|
function loadAddon() {
|
||||||
|
if (CodeMirror.lint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return loadScript([
|
||||||
|
'/vendor/codemirror/addon/lint/lint.css',
|
||||||
|
'/msgbox/msgbox.css',
|
||||||
|
'/vendor/codemirror/addon/lint/lint.js',
|
||||||
|
'/edit/lint-codemirror-helper.js',
|
||||||
|
'/msgbox/msgbox.js'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return onDOMscripted(scripts)
|
|
||||||
.then(() => (loadLinterAssets.loadingName = null));
|
|
||||||
}
|
}
|
||||||
|
|
181
edit/regexp-tester.js
Normal file
181
edit/regexp-tester.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/* global showHelp */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var regExpTester = (() => {
|
||||||
|
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||||
|
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||||
|
const cachedRegexps = new Map();
|
||||||
|
let currentRegexps = [];
|
||||||
|
let isInit = false;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
isInit = true;
|
||||||
|
chrome.tabs.onUpdated.addListener(onTabUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninit() {
|
||||||
|
chrome.tabs.onUpdated.removeListener(onTabUpdate);
|
||||||
|
isInit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTabUpdate(tabId, info) {
|
||||||
|
if (info.url) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShowed() {
|
||||||
|
return Boolean($('.regexp-report'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(state = !isShowed()) {
|
||||||
|
if (state && !isShowed()) {
|
||||||
|
if (!isInit) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
showHelp('', $element({className: 'regexp-report'}));
|
||||||
|
} else if (!state && isShowed()) {
|
||||||
|
if (isInit) {
|
||||||
|
uninit();
|
||||||
|
}
|
||||||
|
// TODO: need a closeHelp function
|
||||||
|
$('#help-popup .dismiss').onclick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(newRegexps) {
|
||||||
|
if (!isShowed()) {
|
||||||
|
if (isInit) {
|
||||||
|
uninit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newRegexps) {
|
||||||
|
currentRegexps = newRegexps;
|
||||||
|
}
|
||||||
|
const regexps = currentRegexps.map(text => {
|
||||||
|
const rxData = Object.assign({text}, cachedRegexps.get(text));
|
||||||
|
if (!rxData.urls) {
|
||||||
|
cachedRegexps.set(text, Object.assign(rxData, {
|
||||||
|
// imitate buggy Stylish-for-chrome, see detectSloppyRegexps()
|
||||||
|
rx: tryRegExp('^' + text + '$'),
|
||||||
|
urls: new Map(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return rxData;
|
||||||
|
});
|
||||||
|
const getMatchInfo = m => m && {text: m[0], pos: m.index};
|
||||||
|
queryTabs().then(tabs => {
|
||||||
|
const supported = tabs.map(tab => tab.url)
|
||||||
|
.filter(url => URLS.supported(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) || getMatchInfo(url.match(rx));
|
||||||
|
if (match) {
|
||||||
|
urlsNow.set(url, match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rxData.urls = urlsNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stats = {
|
||||||
|
full: {data: [], label: t('styleRegexpTestFull')},
|
||||||
|
partial: {data: [], label: [
|
||||||
|
t('styleRegexpTestPartial'),
|
||||||
|
template.regexpTestPartial.cloneNode(true),
|
||||||
|
]},
|
||||||
|
none: {data: [], label: t('styleRegexpTestNone')},
|
||||||
|
invalid: {data: [], label: t('styleRegexpTestInvalid')},
|
||||||
|
};
|
||||||
|
// collect stats
|
||||||
|
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 = $element({tag: 'img', src: faviconUrl});
|
||||||
|
if (match.text.length === url.length) {
|
||||||
|
full.push($element({appendChild: [
|
||||||
|
icon,
|
||||||
|
url,
|
||||||
|
]}));
|
||||||
|
} else {
|
||||||
|
partial.push($element({appendChild: [
|
||||||
|
icon,
|
||||||
|
url.substr(0, match.pos),
|
||||||
|
$element({tag: 'mark', textContent: match.text}),
|
||||||
|
url.substr(match.pos + match.text.length),
|
||||||
|
]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (full.length) {
|
||||||
|
stats.full.data.push({text, urls: full});
|
||||||
|
}
|
||||||
|
if (partial.length) {
|
||||||
|
stats.partial.data.push({text, urls: partial});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// render stats
|
||||||
|
const report = $element({className: 'regexp-report'});
|
||||||
|
const br = $element({tag: 'br'});
|
||||||
|
for (const type in stats) {
|
||||||
|
// top level groups: full, partial, none, invalid
|
||||||
|
const {label, data} = stats[type];
|
||||||
|
if (!data.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const block = report.appendChild($element({
|
||||||
|
tag: 'details',
|
||||||
|
open: true,
|
||||||
|
dataset: {type},
|
||||||
|
appendChild: $element({tag: 'summary', appendChild: label}),
|
||||||
|
}));
|
||||||
|
// 2nd level: regexp text
|
||||||
|
for (const {text, urls} of data) {
|
||||||
|
if (urls) {
|
||||||
|
// type is partial or full
|
||||||
|
block.appendChild($element({
|
||||||
|
tag: 'details',
|
||||||
|
open: true,
|
||||||
|
appendChild: [
|
||||||
|
$element({tag: 'summary', textContent: text}),
|
||||||
|
// 3rd level: tab urls
|
||||||
|
...urls,
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// type is none or invalid
|
||||||
|
block.appendChild(document.createTextNode(text));
|
||||||
|
block.appendChild(br.cloneNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showHelp(t('styleRegexpTestTitle'), report);
|
||||||
|
|
||||||
|
$('.regexp-report').onclick = event => {
|
||||||
|
const target = event.target.closest('a, .regexp-report div');
|
||||||
|
if (target) {
|
||||||
|
openURL({url: target.href || target.textContent});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {toggle, update};
|
||||||
|
})();
|
293
edit/source-editor.js
Normal file
293
edit/source-editor.js
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
/* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */
|
||||||
|
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
|
||||||
|
/* global hotkeyRerouter setupAutocomplete setupOptionsExpand */
|
||||||
|
/* global editors linterConfig updateLinter regExpTester mozParser */
|
||||||
|
/* global makeLink createAppliesToLineWidget messageBox */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createSourceEditor(style) {
|
||||||
|
// a flag for isTouched()
|
||||||
|
let hadBeenSaved = false;
|
||||||
|
|
||||||
|
// draw HTML
|
||||||
|
$('#sections').textContent = '';
|
||||||
|
$('#name').disabled = true;
|
||||||
|
$('#mozilla-format-heading').parentNode.remove();
|
||||||
|
|
||||||
|
$('#sections').appendChild(
|
||||||
|
$element({className: 'single-editor', appendChild: [
|
||||||
|
$element({tag: 'textarea'})
|
||||||
|
]})
|
||||||
|
);
|
||||||
|
|
||||||
|
$('#header').appendChild($element({
|
||||||
|
id: 'footer',
|
||||||
|
appendChild: makeLink('https://github.com/openstyles/stylus/wiki/Usercss', t('externalUsercssDocument'))
|
||||||
|
}));
|
||||||
|
|
||||||
|
setupOptionsExpand();
|
||||||
|
|
||||||
|
// dirty reporter
|
||||||
|
const dirty = dirtyReporter();
|
||||||
|
dirty.onChange(() => {
|
||||||
|
const DIRTY = dirty.isDirty();
|
||||||
|
document.body.classList.toggle('dirty', DIRTY);
|
||||||
|
$('#save-button').disabled = !DIRTY;
|
||||||
|
updateTitle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// normalize style
|
||||||
|
if (!style.id) {
|
||||||
|
setupNewStyle(style);
|
||||||
|
} else {
|
||||||
|
// style might be an object reference to background page
|
||||||
|
style = deepCopy(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw CodeMirror
|
||||||
|
$('#sections textarea').value = style.sourceCode;
|
||||||
|
const cm = CodeMirror.fromTextArea($('#sections textarea'));
|
||||||
|
// too many functions depend on this global
|
||||||
|
editors.push(cm);
|
||||||
|
|
||||||
|
// draw metas info
|
||||||
|
updateMeta();
|
||||||
|
initHooks();
|
||||||
|
initAppliesToLineWidget();
|
||||||
|
|
||||||
|
// setup linter
|
||||||
|
initLint();
|
||||||
|
initLinterSwitch();
|
||||||
|
|
||||||
|
function initAppliesToLineWidget() {
|
||||||
|
const PREF_NAME = 'editor.appliesToLineWidget';
|
||||||
|
const widget = createAppliesToLineWidget(cm);
|
||||||
|
const optionEl = buildOption();
|
||||||
|
|
||||||
|
$('#options').insertBefore(optionEl, $('#options > .option.aligned'));
|
||||||
|
widget.toggle(prefs.get(PREF_NAME));
|
||||||
|
prefs.subscribe([PREF_NAME], (key, value) => {
|
||||||
|
widget.toggle(value);
|
||||||
|
optionEl.checked = value;
|
||||||
|
});
|
||||||
|
optionEl.addEventListener('change', e => {
|
||||||
|
prefs.set(PREF_NAME, e.target.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildOption() {
|
||||||
|
return $element({className: 'option', appendChild: [
|
||||||
|
$element({
|
||||||
|
tag: 'input',
|
||||||
|
type: 'checkbox',
|
||||||
|
id: PREF_NAME,
|
||||||
|
checked: prefs.get(PREF_NAME)
|
||||||
|
}),
|
||||||
|
$element({
|
||||||
|
tag: 'label',
|
||||||
|
htmlFor: PREF_NAME,
|
||||||
|
textContent: ' ' + t('appliesLineWidgetLabel'),
|
||||||
|
title: t('appliesLineWidgetWarning')
|
||||||
|
})
|
||||||
|
]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLinterSwitch() {
|
||||||
|
const linterEl = $('#editor.linter');
|
||||||
|
cm.on('optionChange', (cm, option) => {
|
||||||
|
if (option !== 'mode') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateLinter();
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
linterEl.addEventListener('change', update);
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
linterEl.value = linterConfig.getDefault();
|
||||||
|
|
||||||
|
const cssLintOption = linterEl.querySelector('[value="csslint"]');
|
||||||
|
if (cm.getOption('mode') !== 'css') {
|
||||||
|
cssLintOption.disabled = true;
|
||||||
|
cssLintOption.title = t('linterCSSLintIncompatible', cm.getOption('mode'));
|
||||||
|
} else {
|
||||||
|
cssLintOption.disabled = false;
|
||||||
|
cssLintOption.title = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupNewStyle(style) {
|
||||||
|
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + '/* Insert code here... */';
|
||||||
|
let section = mozParser.format(style);
|
||||||
|
if (!section.includes('@-moz-document')) {
|
||||||
|
style.sections[0].domains = ['example.com'];
|
||||||
|
section = mozParser.format(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCode = `/* ==UserStyle==
|
||||||
|
@name New Style - ${Date.now()}
|
||||||
|
@namespace github.com/openstyles/stylus
|
||||||
|
@version 0.1.0
|
||||||
|
@description A new userstyle
|
||||||
|
@author Me
|
||||||
|
==/UserStyle== */
|
||||||
|
|
||||||
|
${section}
|
||||||
|
`;
|
||||||
|
dirty.modify('source', '', sourceCode);
|
||||||
|
style.sourceCode = sourceCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHooks() {
|
||||||
|
// sidebar commands
|
||||||
|
$('#save-button').onclick = save;
|
||||||
|
$('#beautify').onclick = beautify;
|
||||||
|
$('#keyMap-help').onclick = showKeyMapHelp;
|
||||||
|
$('#toggle-style-help').onclick = showToggleStyleHelp;
|
||||||
|
$('#cancel-button').onclick = goBackToManage;
|
||||||
|
|
||||||
|
// enable
|
||||||
|
$('#enabled').onchange = e => {
|
||||||
|
const value = e.target.checked;
|
||||||
|
dirty.modify('enabled', style.enabled, value);
|
||||||
|
style.enabled = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// source
|
||||||
|
cm.on('change', () => {
|
||||||
|
const value = cm.getValue();
|
||||||
|
dirty.modify('source', style.sourceCode, value);
|
||||||
|
style.sourceCode = value;
|
||||||
|
|
||||||
|
updateLintReportIfEnabled(cm);
|
||||||
|
});
|
||||||
|
|
||||||
|
// hotkeyRerouter
|
||||||
|
cm.on('focus', () => {
|
||||||
|
hotkeyRerouter.setState(false);
|
||||||
|
});
|
||||||
|
cm.on('blur', () => {
|
||||||
|
hotkeyRerouter.setState(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// autocomplete
|
||||||
|
if (prefs.get('editor.autocompleteOnTyping')) {
|
||||||
|
setupAutocomplete(cm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMeta() {
|
||||||
|
$('#name').value = style.name;
|
||||||
|
$('#enabled').checked = style.enabled;
|
||||||
|
$('#url').href = style.url;
|
||||||
|
const {usercssData: {preprocessor} = {}} = style;
|
||||||
|
cm.setPreprocessor(preprocessor);
|
||||||
|
// beautify only works with regular CSS
|
||||||
|
$('#beautify').disabled = cm.getOption('mode') !== 'css';
|
||||||
|
updateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTitle() {
|
||||||
|
// title depends on dirty and style meta
|
||||||
|
if (!style.id) {
|
||||||
|
document.title = t('addStyleTitle');
|
||||||
|
} else {
|
||||||
|
document.title = (dirty.isDirty() ? '* ' : '') + t('editStyleTitle', [style.name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStyle(newStyle) {
|
||||||
|
if (!style.id && newStyle.id) {
|
||||||
|
history.replaceState({}, '', `?id=${newStyle.id}`);
|
||||||
|
}
|
||||||
|
style = deepCopy(newStyle);
|
||||||
|
updateMeta();
|
||||||
|
if (style.sourceCode !== cm.getValue()) {
|
||||||
|
const cursor = cm.getCursor();
|
||||||
|
cm.setValue(style.sourceCode);
|
||||||
|
cm.setCursor(cursor);
|
||||||
|
}
|
||||||
|
dirty.clear();
|
||||||
|
hadBeenSaved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStyleDirty(newStyle) {
|
||||||
|
dirty.clear();
|
||||||
|
dirty.modify('source', newStyle.sourceCode, style.sourceCode);
|
||||||
|
dirty.modify('enabled', newStyle.enabled, style.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStyle() {
|
||||||
|
const value = !style.enabled;
|
||||||
|
dirty.modify('enabled', style.enabled, value);
|
||||||
|
style.enabled = value;
|
||||||
|
updateMeta();
|
||||||
|
// save when toggle enable state?
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!dirty.isDirty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return onBackgroundReady()
|
||||||
|
.then(() => BG.usercssHelper.save({
|
||||||
|
reason: 'editSave',
|
||||||
|
id: style.id,
|
||||||
|
enabled: style.enabled,
|
||||||
|
sourceCode: style.sourceCode
|
||||||
|
}))
|
||||||
|
.then(replaceStyle)
|
||||||
|
.then(() => {
|
||||||
|
hadBeenSaved = true;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
const contents = [String(err)];
|
||||||
|
if (Number.isInteger(err.index)) {
|
||||||
|
const pos = cm.posFromIndex(err.index);
|
||||||
|
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
|
||||||
|
contents.push($element({
|
||||||
|
tag: 'pre',
|
||||||
|
textContent: drawLinePointer(pos)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
console.error(err);
|
||||||
|
messageBox.alert(contents);
|
||||||
|
});
|
||||||
|
|
||||||
|
function drawLinePointer(pos) {
|
||||||
|
const SIZE = 60;
|
||||||
|
const line = cm.getLine(pos.line);
|
||||||
|
const pointer = ' '.repeat(pos.ch) + '^';
|
||||||
|
const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0);
|
||||||
|
const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length);
|
||||||
|
const leftPad = start !== 0 ? '...' : '';
|
||||||
|
const rightPad = end !== line.length ? '...' : '';
|
||||||
|
return leftPad + line.slice(start, end) + rightPad + '\n' +
|
||||||
|
' '.repeat(leftPad.length) + pointer.slice(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTouched() {
|
||||||
|
// indicate that the editor had been touched by the user
|
||||||
|
return dirty.isDirty() || hadBeenSaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceMeta(newStyle) {
|
||||||
|
style.enabled = newStyle.enabled;
|
||||||
|
dirty.clear('enabled');
|
||||||
|
updateMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
replaceStyle,
|
||||||
|
replaceMeta,
|
||||||
|
setStyleDirty,
|
||||||
|
save,
|
||||||
|
toggleStyle,
|
||||||
|
isDirty: dirty.isDirty,
|
||||||
|
getStyle: () => style,
|
||||||
|
isTouched
|
||||||
|
};
|
||||||
|
}
|
95
edit/util.js
Normal file
95
edit/util.js
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function dirtyReporter() {
|
||||||
|
const dirty = new Map();
|
||||||
|
const onchanges = [];
|
||||||
|
|
||||||
|
function add(obj, value) {
|
||||||
|
const saved = dirty.get(obj);
|
||||||
|
if (!saved) {
|
||||||
|
dirty.set(obj, {type: 'add', newValue: value});
|
||||||
|
} else if (saved.type === 'remove') {
|
||||||
|
if (saved.savedValue === value) {
|
||||||
|
dirty.delete(obj);
|
||||||
|
} else {
|
||||||
|
saved.newValue = value;
|
||||||
|
saved.type = 'modify';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(obj, value) {
|
||||||
|
const saved = dirty.get(obj);
|
||||||
|
if (!saved) {
|
||||||
|
dirty.set(obj, {type: 'remove', savedValue: value});
|
||||||
|
} else if (saved.type === 'add') {
|
||||||
|
dirty.delete(obj);
|
||||||
|
} else if (saved.type === 'modify') {
|
||||||
|
saved.type = 'remove';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function modify(obj, oldValue, newValue) {
|
||||||
|
const saved = dirty.get(obj);
|
||||||
|
if (!saved) {
|
||||||
|
if (oldValue !== newValue) {
|
||||||
|
dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
||||||
|
}
|
||||||
|
} else if (saved.type === 'modify') {
|
||||||
|
if (saved.savedValue === newValue) {
|
||||||
|
dirty.delete(obj);
|
||||||
|
} else {
|
||||||
|
saved.newValue = newValue;
|
||||||
|
}
|
||||||
|
} else if (saved.type === 'add') {
|
||||||
|
saved.newValue = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(obj) {
|
||||||
|
if (obj === undefined) {
|
||||||
|
dirty.clear();
|
||||||
|
} else {
|
||||||
|
dirty.delete(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirty() {
|
||||||
|
return dirty.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(cb) {
|
||||||
|
// make sure the callback doesn't throw
|
||||||
|
onchanges.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrap(obj) {
|
||||||
|
for (const key of ['add', 'remove', 'modify', 'clear']) {
|
||||||
|
obj[key] = trackChange(obj[key]);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitChange() {
|
||||||
|
for (const cb of onchanges) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackChange(fn) {
|
||||||
|
return function () {
|
||||||
|
const dirty = isDirty();
|
||||||
|
const result = fn.apply(null, arguments);
|
||||||
|
if (dirty !== isDirty()) {
|
||||||
|
emitChange();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function has(key) {
|
||||||
|
return dirty.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap({add, remove, modify, clear, isDirty, onChange, has});
|
||||||
|
}
|
92
install-usercss.html
Normal file
92
install-usercss.html
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Loading...</title>
|
||||||
|
<link rel="stylesheet" href="/install-usercss/install-usercss.css">
|
||||||
|
<script src="/js/messaging.js"></script>
|
||||||
|
<script src="/js/prefs.js"></script>
|
||||||
|
<script src="/js/dom.js"></script>
|
||||||
|
<script src="/js/localization.js"></script>
|
||||||
|
<script src="/vendor/node-semver/semver.js"></script>
|
||||||
|
|
||||||
|
<script src="/msgbox/msgbox.js"></script>
|
||||||
|
<link rel="stylesheet" href="/msgbox/msgbox.css"></script>
|
||||||
|
|
||||||
|
<!-- FIXME: do we need all of these? -->
|
||||||
|
<script src="/vendor/codemirror/lib/codemirror.js"></script>
|
||||||
|
<script src="/vendor/codemirror/keymap/sublime.js"></script>
|
||||||
|
<script src="/vendor/codemirror/keymap/emacs.js"></script>
|
||||||
|
<script src="/vendor/codemirror/keymap/vim.js"></script>
|
||||||
|
|
||||||
|
<script src="/edit/codemirror-default.js"></script>
|
||||||
|
<link rel="stylesheet" href="/edit/codemirror-default.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/vendor/codemirror/lib/codemirror.css">
|
||||||
|
<script src="/vendor/codemirror/mode/css/css.js"></script>
|
||||||
|
<link rel="stylesheet" href="/vendor/codemirror/addon/dialog/dialog.css">
|
||||||
|
<link rel="stylesheet" href="/vendor/codemirror/addon/search/matchesonscrollbar.css">
|
||||||
|
<script src="/vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||||
|
<script src="/vendor-overwrites/codemirror/addon/search/match-highlighter.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/search/searchcursor.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/search/search.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/comment/comment.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/selection/active-line.js"></script>
|
||||||
|
<link rel="stylesheet" href="/vendor/codemirror/addon/fold/foldgutter.css" />
|
||||||
|
<script src="/vendor/codemirror/addon/fold/foldcode.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/fold/foldgutter.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/fold/brace-fold.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
|
<link rel="stylesheet" href="/vendor/codemirror/addon/lint/lint.css" />
|
||||||
|
<link rel="stylesheet" href="/vendor/codemirror/addon/hint/show-hint.css" />
|
||||||
|
<script src="/vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||||
|
<script src="/vendor/codemirror/addon/mode/loadmode.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>
|
||||||
|
<span class="meta-name"></span>
|
||||||
|
<small class="meta-version"></small>
|
||||||
|
</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="install" i18n-text="installButton"></button>
|
||||||
|
<label class="set-update-url">
|
||||||
|
<input type="checkbox">
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
<label class="live-reload">
|
||||||
|
<input type="checkbox">
|
||||||
|
<span i18n-text="liveReloadLabel"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="meta-description"></p>
|
||||||
|
<div>
|
||||||
|
<h3 i18n-text="author"></h3>
|
||||||
|
<span class="meta-author"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 i18n-text="license"></h3>
|
||||||
|
<span class="meta-license"></span>
|
||||||
|
</div>
|
||||||
|
<h3 i18n-text="appliesLabel"></h3>
|
||||||
|
<ul class="applies-to">
|
||||||
|
</ul>
|
||||||
|
<div class="external-link"></div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="code">
|
||||||
|
<textarea></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/content/util.js"></script>
|
||||||
|
<script src="/install-usercss/install-usercss.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
121
install-usercss/install-usercss.css
Normal file
121
install-usercss/install-usercss.css
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font: 12px arial, sans-serif;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #000;
|
||||||
|
transition: color .5s;
|
||||||
|
text-decoration-skip: ink;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.icon,
|
||||||
|
svg.icon {
|
||||||
|
height: 1.4em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex: 0 0 280px;
|
||||||
|
padding: 15px;
|
||||||
|
border-right: 1px dashed #aaa;
|
||||||
|
box-shadow: 0 0 50px -18px black;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 small {
|
||||||
|
font-size: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-version::before {
|
||||||
|
content: " v";
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
padding: 3px 6px;
|
||||||
|
border: 1px dashed black;
|
||||||
|
|
||||||
|
border-color: #ef6969;
|
||||||
|
background: #ffe2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .warning {
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions label {
|
||||||
|
max-width: fit-content;
|
||||||
|
max-width: -moz-fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions label input {
|
||||||
|
margin: 0 0.5em 0 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions label span {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external > * {
|
||||||
|
margin: 0 7.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > :first-child {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > :last-child {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .code,
|
||||||
|
.main .CodeMirror {
|
||||||
|
height: 100%;
|
||||||
|
}
|
299
install-usercss/install-usercss.js
Normal file
299
install-usercss/install-usercss.js
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
/* global CodeMirror semverCompare makeLink closeCurrentTab runtimeSend */
|
||||||
|
/* global messageBox */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
let liveReload = false;
|
||||||
|
let installed = false;
|
||||||
|
|
||||||
|
const port = chrome.tabs.connect(
|
||||||
|
Number(params.get('tabId')),
|
||||||
|
{name: 'usercss-install', frameId: 0}
|
||||||
|
);
|
||||||
|
port.postMessage({method: 'getSourceCode'});
|
||||||
|
port.onMessage.addListener(msg => {
|
||||||
|
switch (msg.method) {
|
||||||
|
case 'getSourceCodeResponse':
|
||||||
|
if (msg.error) {
|
||||||
|
messageBox.alert(msg.error);
|
||||||
|
} else {
|
||||||
|
initSourceCode(msg.sourceCode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'sourceCodeChanged':
|
||||||
|
if (msg.error) {
|
||||||
|
messageBox.alert(msg.error);
|
||||||
|
} else {
|
||||||
|
liveReloadUpdate(msg.sourceCode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
port.onDisconnect.addListener(closeCurrentTab);
|
||||||
|
|
||||||
|
const cm = CodeMirror.fromTextArea($('.code textarea'), {readOnly: true});
|
||||||
|
let liveReloadPending = Promise.resolve();
|
||||||
|
|
||||||
|
function liveReloadUpdate(sourceCode) {
|
||||||
|
liveReloadPending = liveReloadPending.then(() => {
|
||||||
|
const scrollInfo = cm.getScrollInfo();
|
||||||
|
const cursor = cm.getCursor();
|
||||||
|
cm.setValue(sourceCode);
|
||||||
|
cm.setCursor(cursor);
|
||||||
|
cm.scrollTo(scrollInfo.left, scrollInfo.top);
|
||||||
|
|
||||||
|
return runtimeSend({
|
||||||
|
id: installed.id,
|
||||||
|
method: 'saveUsercss',
|
||||||
|
reason: 'update',
|
||||||
|
sourceCode
|
||||||
|
}).then(updateMeta)
|
||||||
|
.catch(showError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMeta(style, dup) {
|
||||||
|
$$('.main .warning').forEach(e => e.remove());
|
||||||
|
|
||||||
|
const data = style.usercssData;
|
||||||
|
const dupData = dup && dup.usercssData;
|
||||||
|
const versionTest = dup && semverCompare(data.version, dupData.version);
|
||||||
|
|
||||||
|
// update editor
|
||||||
|
cm.setPreprocessor(data.preprocessor);
|
||||||
|
|
||||||
|
// update metas
|
||||||
|
document.title = `${installButtonLabel()} ${data.name}`;
|
||||||
|
|
||||||
|
$('.install').textContent = installButtonLabel();
|
||||||
|
$('.set-update-url').title = dup && dup.updateUrl && t('installUpdateFrom', dup.updateUrl) || '';
|
||||||
|
$('.meta-name').textContent = data.name;
|
||||||
|
$('.meta-version').textContent = data.version;
|
||||||
|
$('.meta-description').textContent = data.description;
|
||||||
|
|
||||||
|
if (data.author) {
|
||||||
|
$('.meta-author').parentNode.style.display = '';
|
||||||
|
$('.meta-author').textContent = '';
|
||||||
|
$('.meta-author').appendChild(makeAuthor(data.author));
|
||||||
|
} else {
|
||||||
|
$('.meta-author').parentNode.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.meta-license').parentNode.style.display = data.license ? '' : 'none';
|
||||||
|
$('.meta-license').textContent = data.license;
|
||||||
|
|
||||||
|
$('.applies-to').textContent = '';
|
||||||
|
getAppliesTo(style).forEach(pattern =>
|
||||||
|
$('.applies-to').appendChild($element({tag: 'li', textContent: pattern}))
|
||||||
|
);
|
||||||
|
|
||||||
|
$('.external-link').textContent = '';
|
||||||
|
const externalLink = makeExternalLink();
|
||||||
|
if (externalLink) {
|
||||||
|
$('.external-link').appendChild(externalLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAuthor(text) {
|
||||||
|
const match = text.match(/^(.+?)(?:\s+<(.+?)>)?(?:\s+\((.+?)\))$/);
|
||||||
|
if (!match) {
|
||||||
|
return document.createTextNode(text);
|
||||||
|
}
|
||||||
|
const [, name, email, url] = match;
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
if (email) {
|
||||||
|
frag.appendChild(makeLink(`mailto:${email}`, name));
|
||||||
|
} else {
|
||||||
|
frag.appendChild($element({
|
||||||
|
tag: 'span',
|
||||||
|
textContent: name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
frag.appendChild(makeLink(
|
||||||
|
url,
|
||||||
|
$element({
|
||||||
|
tag: 'svg#svg',
|
||||||
|
viewBox: '0 0 20 20',
|
||||||
|
class: 'icon',
|
||||||
|
appendChild: $element({
|
||||||
|
tag: 'svg#path',
|
||||||
|
d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeExternalLink() {
|
||||||
|
const urls = [];
|
||||||
|
if (data.homepageURL) {
|
||||||
|
urls.push([data.homepageURL, t('externalHomepage')]);
|
||||||
|
}
|
||||||
|
if (data.supportURL) {
|
||||||
|
urls.push([data.supportURL, t('externalSupport')]);
|
||||||
|
}
|
||||||
|
if (urls.length) {
|
||||||
|
return $element({appendChild: [
|
||||||
|
$element({tag: 'h3', textContent: t('externalLink')}),
|
||||||
|
$element({tag: 'ul', appendChild: urls.map(args =>
|
||||||
|
$element({tag: 'li', appendChild: makeLink(...args)})
|
||||||
|
)})
|
||||||
|
]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installButtonLabel() {
|
||||||
|
return t(
|
||||||
|
installed ? 'installButtonInstalled' :
|
||||||
|
!dup ? 'installButton' :
|
||||||
|
versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(err) {
|
||||||
|
$$('.main .warning').forEach(e => e.remove());
|
||||||
|
const main = $('.main');
|
||||||
|
main.insertBefore(buildWarning(err), main.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(style) {
|
||||||
|
const request = Object.assign(style, {
|
||||||
|
method: 'saveUsercss',
|
||||||
|
reason: 'update'
|
||||||
|
});
|
||||||
|
return runtimeSend(request)
|
||||||
|
.then(result => {
|
||||||
|
installed = result;
|
||||||
|
|
||||||
|
$$('.warning')
|
||||||
|
.forEach(el => el.remove());
|
||||||
|
$('.install').disabled = true;
|
||||||
|
$('.install').classList.add('installed');
|
||||||
|
$('.set-update-url input[type=checkbox]').disabled = true;
|
||||||
|
$('.set-update-url').title = result.updateUrl ?
|
||||||
|
t('installUpdateFrom', result.updateUrl) : '';
|
||||||
|
|
||||||
|
updateMeta(result);
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({method: 'openEditor', id: result.id});
|
||||||
|
|
||||||
|
if (!liveReload) {
|
||||||
|
port.postMessage({method: 'closeTab'});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('installed'));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
messageBox.alert(chrome.i18n.getMessage('styleInstallFailed', String(err)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSourceCode(sourceCode) {
|
||||||
|
cm.setValue(sourceCode);
|
||||||
|
runtimeSend({
|
||||||
|
method: 'buildUsercss',
|
||||||
|
sourceCode,
|
||||||
|
checkDup: true
|
||||||
|
}).then(init, initError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initError(err) {
|
||||||
|
$('.main').insertBefore(buildWarning(err), $('.main').childNodes[0]);
|
||||||
|
$('.header').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWarning(err) {
|
||||||
|
return $element({className: 'warning', appendChild: [
|
||||||
|
t('parseUsercssError'),
|
||||||
|
$element({tag: 'pre', textContent: String(err)})
|
||||||
|
]});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init({style, dup}) {
|
||||||
|
const data = style.usercssData;
|
||||||
|
const dupData = dup && dup.usercssData;
|
||||||
|
const versionTest = dup && semverCompare(data.version, dupData.version);
|
||||||
|
|
||||||
|
updateMeta(style, dup);
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
if (versionTest < 0) {
|
||||||
|
$('.actions').parentNode.insertBefore(
|
||||||
|
$element({className: 'warning', textContent: t('versionInvalidOlder')}),
|
||||||
|
$('.actions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$('button.install').onclick = () => {
|
||||||
|
const message = dup ?
|
||||||
|
chrome.i18n.getMessage('styleInstallOverwrite', [
|
||||||
|
data.name, dupData.version, data.version
|
||||||
|
]) :
|
||||||
|
chrome.i18n.getMessage('styleInstall', [data.name]);
|
||||||
|
|
||||||
|
messageBox.confirm(message).then(result => {
|
||||||
|
if (result) {
|
||||||
|
return install(style);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// set updateUrl
|
||||||
|
const setUpdate = $('.set-update-url input[type=checkbox]');
|
||||||
|
const updateUrl = new URL(params.get('updateUrl'));
|
||||||
|
$('.set-update-url > span').textContent = t('installUpdateFromLabel');
|
||||||
|
if (dup && dup.updateUrl === updateUrl.href) {
|
||||||
|
setUpdate.checked = true;
|
||||||
|
// there is no way to "unset" updateUrl, you can only overwrite it.
|
||||||
|
setUpdate.disabled = true;
|
||||||
|
} else if (updateUrl.protocol !== 'file:') {
|
||||||
|
setUpdate.checked = true;
|
||||||
|
style.updateUrl = updateUrl.href;
|
||||||
|
}
|
||||||
|
setUpdate.onchange = e => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
style.updateUrl = updateUrl.href;
|
||||||
|
} else {
|
||||||
|
delete style.updateUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// live reload
|
||||||
|
const setLiveReload = $('.live-reload input[type=checkbox]');
|
||||||
|
if (updateUrl.protocol !== 'file:') {
|
||||||
|
setLiveReload.parentNode.remove();
|
||||||
|
} else {
|
||||||
|
setLiveReload.addEventListener('change', () => {
|
||||||
|
liveReload = setLiveReload.checked;
|
||||||
|
if (installed) {
|
||||||
|
const method = 'liveReload' + (liveReload ? 'Start' : 'Stop');
|
||||||
|
port.postMessage({method});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('installed', () => {
|
||||||
|
if (liveReload) {
|
||||||
|
port.postMessage({method: 'liveReloadStart'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppliesTo(style) {
|
||||||
|
function *_gen() {
|
||||||
|
for (const section of style.sections) {
|
||||||
|
for (const type of ['urls', 'urlPrefixes', 'domains', 'regexps']) {
|
||||||
|
if (section[type]) {
|
||||||
|
yield *section[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = [..._gen()];
|
||||||
|
if (!result.length) {
|
||||||
|
result.push(chrome.i18n.getMessage('appliesToEverything'));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
})();
|
40
js/color-parser.js
Normal file
40
js/color-parser.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var colorParser = (() => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
// https://bugs.webkit.org/show_bug.cgi?id=14563
|
||||||
|
document.head.appendChild(el);
|
||||||
|
|
||||||
|
function parseRGB(color) {
|
||||||
|
const [r, g, b, a = 1] = color.match(/[.\d]+/g).map(Number);
|
||||||
|
return {r, g, b, a};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(color) {
|
||||||
|
el.style.color = color;
|
||||||
|
if (el.style.color === '') {
|
||||||
|
throw new Error(chrome.i18n.getMessage('styleMetaErrorColor', color));
|
||||||
|
}
|
||||||
|
color = getComputedStyle(el).color;
|
||||||
|
el.style.color = '';
|
||||||
|
return parseRGB(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
function format({r, g, b, a = 1}) {
|
||||||
|
if (a === 1) {
|
||||||
|
return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
}
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHex({r, g, b, a = null}) {
|
||||||
|
let hex = '#' + (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).substr(1);
|
||||||
|
if (a !== null) {
|
||||||
|
hex += (0x100 + Math.floor(a * 255)).toString(16).substr(1);
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {parse, format, formatHex};
|
||||||
|
})();
|
118
js/dom.js
118
js/dom.js
|
@ -33,10 +33,19 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection])
|
||||||
// enqueue after DOMContentLoaded/load events
|
// enqueue after DOMContentLoaded/load events
|
||||||
setTimeout(addTooltipsToEllipsized);
|
setTimeout(addTooltipsToEllipsized);
|
||||||
// throttle on continuous resizing
|
// throttle on continuous resizing
|
||||||
window.addEventListener('resize', () => debounce(addTooltipsToEllipsized, 100));
|
let timer;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(addTooltipsToEllipsized, 100);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onDOMready().then(() => $('#firefox-transitions-bug-suppressor').remove());
|
onDOMready().then(() => {
|
||||||
|
const el = $('#firefox-transitions-bug-suppressor');
|
||||||
|
if (el) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!chrome.app) {
|
if (!chrome.app) {
|
||||||
// die if unable to access BG directly
|
// die if unable to access BG directly
|
||||||
|
@ -80,95 +89,6 @@ function onDOMready() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onDOMscripted(scripts) {
|
|
||||||
const queue = onDOMscripted.queue = onDOMscripted.queue || [];
|
|
||||||
if (scripts) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
addResolver(resolve);
|
|
||||||
queue.push(...scripts.filter(el => !queue.includes(el)));
|
|
||||||
loadNextScript();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (queue.length) {
|
|
||||||
return new Promise(resolve => addResolver(resolve));
|
|
||||||
}
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
if (onDOMscripted.resolveOnReady) {
|
|
||||||
onDOMscripted.resolveOnReady.forEach(r => r());
|
|
||||||
onDOMscripted.resolveOnReady = null;
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return onDOMready().then(onDOMscripted);
|
|
||||||
|
|
||||||
function loadNextScript() {
|
|
||||||
const empty = !queue.length;
|
|
||||||
const next = !empty && queue.shift();
|
|
||||||
if (empty) {
|
|
||||||
onDOMscripted();
|
|
||||||
} else if (typeof next === 'function') {
|
|
||||||
Promise.resolve(next())
|
|
||||||
.then(loadNextScript);
|
|
||||||
} else {
|
|
||||||
Promise.all(
|
|
||||||
(next instanceof Array ? next : [next]).map(next =>
|
|
||||||
typeof next === 'function'
|
|
||||||
? next()
|
|
||||||
: injectScript({src: next, async: true})
|
|
||||||
)
|
|
||||||
).then(loadNextScript);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addResolver(r) {
|
|
||||||
if (!onDOMscripted.resolveOnReady) {
|
|
||||||
onDOMscripted.resolveOnReady = [];
|
|
||||||
}
|
|
||||||
onDOMscripted.resolveOnReady.push(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function injectScript(properties) {
|
|
||||||
if (typeof properties === 'string') {
|
|
||||||
properties = {src: properties};
|
|
||||||
}
|
|
||||||
if (!properties || !properties.src) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (injectScript.cache) {
|
|
||||||
if (injectScript.cache.has(properties.src)) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
injectScript.cache = new Set();
|
|
||||||
}
|
|
||||||
injectScript.cache.add(properties.src);
|
|
||||||
const script = document.head.appendChild(document.createElement('script'));
|
|
||||||
Object.assign(script, properties);
|
|
||||||
if (!properties.onload) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
script.onload = () => {
|
|
||||||
script.onload = null;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function injectCSS(url) {
|
|
||||||
if (!url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.head.appendChild($element({
|
|
||||||
tag: 'link',
|
|
||||||
rel: 'stylesheet',
|
|
||||||
href: url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function scrollElementIntoView(element) {
|
function scrollElementIntoView(element) {
|
||||||
// align to the top/bottom of the visible area if wasn't visible
|
// align to the top/bottom of the visible area if wasn't visible
|
||||||
const bounds = element.getBoundingClientRect();
|
const bounds = element.getBoundingClientRect();
|
||||||
|
@ -272,3 +192,19 @@ function $element(opt) {
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function makeLink(href = '', content) {
|
||||||
|
const opt = {
|
||||||
|
tag: 'a',
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener'
|
||||||
|
};
|
||||||
|
if (typeof href === 'object') {
|
||||||
|
Object.assign(opt, href);
|
||||||
|
} else {
|
||||||
|
opt.href = href;
|
||||||
|
opt.appendChild = content;
|
||||||
|
}
|
||||||
|
return $element(opt);
|
||||||
|
}
|
||||||
|
|
|
@ -103,7 +103,11 @@ function tNodeList(nodes) {
|
||||||
|
|
||||||
function tDocLoader() {
|
function tDocLoader() {
|
||||||
t.DOMParser = new DOMParser();
|
t.DOMParser = new DOMParser();
|
||||||
t.cache = tryJSONparse(localStorage.L10N) || {};
|
t.cache = (() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.L10N);
|
||||||
|
} catch (e) {}
|
||||||
|
})() || {};
|
||||||
|
|
||||||
// reset L10N cache on UI language change
|
// reset L10N cache on UI language change
|
||||||
const UIlang = chrome.i18n.getUILanguage();
|
const UIlang = chrome.i18n.getUILanguage();
|
||||||
|
|
|
@ -382,15 +382,47 @@ function deleteStyleSafe({id, notify = true} = {}) {
|
||||||
|
|
||||||
function download(url) {
|
function download(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
url = new URL(url);
|
||||||
|
const TIMEOUT = 10000;
|
||||||
|
const options = {
|
||||||
|
method: url.search ? 'POST' : 'GET',
|
||||||
|
body: url.search ? url.search.slice(1) : null,
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (url.protocol === 'file:' && FIREFOX) {
|
||||||
|
// https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
|
||||||
|
options.mode = 'same-origin';
|
||||||
|
// FIXME: add FetchController when it is available.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/FetchController/abort
|
||||||
|
let timer;
|
||||||
|
fetch(url.href, {mode: 'same-origin'})
|
||||||
|
.then(r => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (r.status !== 200) {
|
||||||
|
throw r.status;
|
||||||
|
}
|
||||||
|
return r.text();
|
||||||
|
})
|
||||||
|
.then(resolve, reject);
|
||||||
|
timer = setTimeout(
|
||||||
|
() => reject(new Error(`Fetch URL timeout: ${url.href}`)),
|
||||||
|
TIMEOUT
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.timeout = 10e3;
|
xhr.timeout = TIMEOUT;
|
||||||
xhr.onloadend = () => (xhr.status === 200
|
xhr.onload = () => (xhr.status === 200 || url.protocol === 'file:'
|
||||||
? resolve(xhr.responseText)
|
? resolve(xhr.responseText)
|
||||||
: reject(xhr.status));
|
: reject(xhr.status));
|
||||||
const [mainUrl, query] = url.split('?');
|
xhr.onerror = reject;
|
||||||
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
xhr.open(options.method, url.href, true);
|
||||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
for (const key of Object.keys(options.headers)) {
|
||||||
xhr.send(query);
|
xhr.setRequestHeader(key, options.headers[key]);
|
||||||
|
}
|
||||||
|
xhr.send(options.body);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -400,3 +432,26 @@ function invokeOrPostpone(isInvoke, fn, ...args) {
|
||||||
? fn(...args)
|
? fn(...args)
|
||||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function openEditor(id) {
|
||||||
|
let url = '/edit.html';
|
||||||
|
if (id) {
|
||||||
|
url += `?id=${id}`;
|
||||||
|
}
|
||||||
|
if (prefs.get('openEditInWindow')) {
|
||||||
|
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
||||||
|
} else {
|
||||||
|
openURL({url});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function closeCurrentTab() {
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1409375
|
||||||
|
getOwnTab().then(tab => {
|
||||||
|
if (tab) {
|
||||||
|
chrome.tabs.remove(tab.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
145
js/moz-parser.js
Normal file
145
js/moz-parser.js
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/* global parserlib, loadScript */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var mozParser = (() => {
|
||||||
|
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||||
|
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||||
|
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
|
||||||
|
|
||||||
|
function backtrackTo(parser, tokenType, startEnd) {
|
||||||
|
const tokens = parser._tokenStream._lt;
|
||||||
|
for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) {
|
||||||
|
if (tokens[i].type === tokenType) {
|
||||||
|
return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimNewLines(s) {
|
||||||
|
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMozFormat(mozStyle) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parser = new parserlib.css.Parser();
|
||||||
|
const lines = mozStyle.split('\n');
|
||||||
|
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
|
||||||
|
const errors = [];
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
parser.addListener('startdocument', function (e) {
|
||||||
|
const lastSection = sectionStack[sectionStack.length - 1];
|
||||||
|
let outerText = getRange(lastSection.start, (--e.col, e));
|
||||||
|
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
|
||||||
|
const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')};
|
||||||
|
// move last comment before @-moz-document inside the section
|
||||||
|
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
|
||||||
|
section.code = gapComment[1] + '\n';
|
||||||
|
outerText = trimNewLines(outerText.substring(0, gapComment.index));
|
||||||
|
}
|
||||||
|
if (outerText.trim()) {
|
||||||
|
lastSection.code = outerText;
|
||||||
|
doAddSection(lastSection);
|
||||||
|
lastSection.code = '';
|
||||||
|
}
|
||||||
|
for (const f of e.functions) {
|
||||||
|
const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/);
|
||||||
|
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
|
||||||
|
errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const aType = CssToProperty[m[1]];
|
||||||
|
const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\');
|
||||||
|
(section[aType] = section[aType] || []).push(aValue);
|
||||||
|
}
|
||||||
|
sectionStack.push(section);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.addListener('enddocument', function () {
|
||||||
|
const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start');
|
||||||
|
const section = sectionStack.pop();
|
||||||
|
const lastSection = sectionStack[sectionStack.length - 1];
|
||||||
|
section.code += getRange(section.start, end);
|
||||||
|
lastSection.start = (++end.col, end);
|
||||||
|
doAddSection(section);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.addListener('endstylesheet', () => {
|
||||||
|
// add nonclosed outer sections (either broken or the last global one)
|
||||||
|
const lastLine = lines[lines.length - 1];
|
||||||
|
const endOfText = {line: lines.length, col: lastLine.length + 1};
|
||||||
|
const lastSection = sectionStack[sectionStack.length - 1];
|
||||||
|
lastSection.code += getRange(lastSection.start, endOfText);
|
||||||
|
sectionStack.forEach(doAddSection);
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
reject(errors);
|
||||||
|
} else {
|
||||||
|
resolve(sections);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.addListener('error', e => {
|
||||||
|
errors.push(e.line + ':' + e.col + ' ' +
|
||||||
|
e.message.replace(/ at line \d.+$/, ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.parse(mozStyle);
|
||||||
|
|
||||||
|
function getRange(start, end) {
|
||||||
|
const L1 = start.line - 1;
|
||||||
|
const C1 = start.col - 1;
|
||||||
|
const L2 = end.line - 1;
|
||||||
|
const C2 = end.col - 1;
|
||||||
|
if (L1 === L2) {
|
||||||
|
return lines[L1].substr(C1, C2 - C1 + 1);
|
||||||
|
} else {
|
||||||
|
const middle = lines.slice(L1 + 1, L2).join('\n');
|
||||||
|
return lines[L1].substr(C1) + '\n' + middle +
|
||||||
|
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
/* ignore boilerplate NS */
|
||||||
|
if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sections.push(Object.assign({}, section));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Parse mozilla-format userstyle into sections
|
||||||
|
parse(text) {
|
||||||
|
return loadScript('/vendor-overwrites/csslint/csslint-worker.js')
|
||||||
|
.then(() => parseMozFormat(text));
|
||||||
|
},
|
||||||
|
format(style) {
|
||||||
|
return style.sections.map(section => {
|
||||||
|
let cssMds = [];
|
||||||
|
for (const i in propertyToCss) {
|
||||||
|
if (section[i]) {
|
||||||
|
cssMds = cssMds.concat(section[i].map(v =>
|
||||||
|
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
|
||||||
|
}).join('\n\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
|
@ -9,6 +9,7 @@ var prefs = new function Prefs() {
|
||||||
'show-badge': true, // display text on popup menu icon
|
'show-badge': true, // display text on popup menu icon
|
||||||
'disableAll': false, // boss key
|
'disableAll': false, // boss key
|
||||||
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
|
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
|
||||||
|
'newStyleAsUsercss': false, // create new style in usercss format
|
||||||
|
|
||||||
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
|
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
|
||||||
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
|
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
|
||||||
|
@ -51,6 +52,8 @@ var prefs = new function Prefs() {
|
||||||
'editor.autocompleteOnTyping': false, // show autocomplete dropdown on typing a word token
|
'editor.autocompleteOnTyping': false, // show autocomplete dropdown on typing a word token
|
||||||
'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu
|
'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu
|
||||||
|
|
||||||
|
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
|
||||||
|
|
||||||
'iconset': 0, // 0 = dark-themed icon
|
'iconset': 0, // 0 = dark-themed icon
|
||||||
// 1 = light-themed icon
|
// 1 = light-themed icon
|
||||||
|
|
||||||
|
|
46
js/script-loader.js
Normal file
46
js/script-loader.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// loadScript(script: Array<Promise|string>|string): Promise
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var loadScript = (() => {
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
function inject(file) {
|
||||||
|
if (!cache.has(file)) {
|
||||||
|
cache.set(file, doInject(file));
|
||||||
|
}
|
||||||
|
return cache.get(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doInject(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let el;
|
||||||
|
if (file.endsWith('.js')) {
|
||||||
|
el = document.createElement('script');
|
||||||
|
el.src = file;
|
||||||
|
} else {
|
||||||
|
el = document.createElement('link');
|
||||||
|
el.rel = 'stylesheet';
|
||||||
|
el.href = file;
|
||||||
|
}
|
||||||
|
el.onload = () => {
|
||||||
|
el.onload = null;
|
||||||
|
el.onerror = null;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
el.onerror = () => {
|
||||||
|
el.onload = null;
|
||||||
|
el.onerror = null;
|
||||||
|
reject(new Error(`Failed to load script: ${file}`));
|
||||||
|
};
|
||||||
|
document.head.appendChild(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return files => {
|
||||||
|
if (!Array.isArray(files)) {
|
||||||
|
files = [files];
|
||||||
|
}
|
||||||
|
return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f)));
|
||||||
|
};
|
||||||
|
})();
|
540
js/usercss.js
Normal file
540
js/usercss.js
Normal file
|
@ -0,0 +1,540 @@
|
||||||
|
/* global loadScript mozParser semverCompare colorParser */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var usercss = (() => {
|
||||||
|
// true for global, false for private
|
||||||
|
const METAS = {
|
||||||
|
__proto__: null,
|
||||||
|
author: true,
|
||||||
|
advanced: false,
|
||||||
|
description: true,
|
||||||
|
homepageURL: false,
|
||||||
|
// icon: false,
|
||||||
|
license: false,
|
||||||
|
name: true,
|
||||||
|
namespace: false,
|
||||||
|
// noframes: false,
|
||||||
|
preprocessor: false,
|
||||||
|
supportURL: false,
|
||||||
|
'var': false,
|
||||||
|
version: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image'];
|
||||||
|
|
||||||
|
const BUILDER = {
|
||||||
|
default: {
|
||||||
|
postprocess(sections, vars) {
|
||||||
|
const varDef =
|
||||||
|
':root {\n' +
|
||||||
|
Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('') +
|
||||||
|
'}\n';
|
||||||
|
|
||||||
|
for (const section of sections) {
|
||||||
|
section.code = varDef + section.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stylus: {
|
||||||
|
preprocess(source, vars) {
|
||||||
|
return loadScript('/vendor/stylus-lang/stylus.min.js').then(() => (
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
stylus(varDef + source).render((err, output) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uso: {
|
||||||
|
preprocess(source, vars) {
|
||||||
|
const pool = new Map();
|
||||||
|
return Promise.resolve(doReplace(source));
|
||||||
|
|
||||||
|
function getValue(name, rgb) {
|
||||||
|
if (!vars.hasOwnProperty(name)) {
|
||||||
|
if (name.endsWith('-rgb')) {
|
||||||
|
return getValue(name.slice(0, -4), true);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (rgb) {
|
||||||
|
if (vars[name].type === 'color') {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
const color = colorParser.parse(vars[name].value);
|
||||||
|
return `${color.r}, ${color.g}, ${color.b}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
||||||
|
// prevent infinite recursion
|
||||||
|
pool.set(name, '');
|
||||||
|
return doReplace(vars[name].value);
|
||||||
|
}
|
||||||
|
return vars[name].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doReplace(text) {
|
||||||
|
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
|
||||||
|
if (!pool.has(name)) {
|
||||||
|
const value = getValue(name);
|
||||||
|
pool.set(name, value === null ? match : value);
|
||||||
|
}
|
||||||
|
return pool.get(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMetaSource(source) {
|
||||||
|
const commentRe = /\/\*[\s\S]*?\*\//g;
|
||||||
|
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
|
||||||
|
|
||||||
|
let m;
|
||||||
|
// iterate through each comment
|
||||||
|
while ((m = commentRe.exec(source))) {
|
||||||
|
const commentSource = source.slice(m.index, m.index + m[0].length);
|
||||||
|
const n = commentSource.match(metaRe);
|
||||||
|
if (n) {
|
||||||
|
return {
|
||||||
|
index: m.index + n.index,
|
||||||
|
text: n[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWord(state, error = 'invalid word') {
|
||||||
|
const match = state.text.slice(state.re.lastIndex).match(/^([\w-]+)\s*/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
state.value = match[1];
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVar(state) {
|
||||||
|
const result = {
|
||||||
|
type: null,
|
||||||
|
label: null,
|
||||||
|
name: null,
|
||||||
|
value: null,
|
||||||
|
default: null,
|
||||||
|
options: null
|
||||||
|
};
|
||||||
|
|
||||||
|
parseWord(state, 'missing type');
|
||||||
|
result.type = state.type = state.value;
|
||||||
|
if (!META_VARS.includes(state.type)) {
|
||||||
|
throw new Error(`unknown type: ${state.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseWord(state, 'missing name');
|
||||||
|
result.name = state.value;
|
||||||
|
|
||||||
|
parseString(state);
|
||||||
|
result.label = state.value;
|
||||||
|
|
||||||
|
if (state.type === 'checkbox') {
|
||||||
|
const match = state.text.slice(state.re.lastIndex).match(/([01])\s+/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('value must be 0 or 1');
|
||||||
|
}
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
result.default = match[1];
|
||||||
|
} else if (state.type === 'select' || (state.type === 'image' && state.key === 'var')) {
|
||||||
|
parseJSONValue(state);
|
||||||
|
if (Array.isArray(state.value)) {
|
||||||
|
result.options = state.value.map(text => createOption(text));
|
||||||
|
} else {
|
||||||
|
result.options = Object.keys(state.value).map(k => createOption(k, state.value[k]));
|
||||||
|
}
|
||||||
|
result.default = result.options[0].name;
|
||||||
|
} else if (state.type === 'dropdown' || state.type === 'image') {
|
||||||
|
if (state.text[state.re.lastIndex] !== '{') {
|
||||||
|
throw new Error('no open {');
|
||||||
|
}
|
||||||
|
result.options = [];
|
||||||
|
state.re.lastIndex++;
|
||||||
|
while (state.text[state.re.lastIndex] !== '}') {
|
||||||
|
const option = {};
|
||||||
|
|
||||||
|
parseStringUnquoted(state);
|
||||||
|
option.name = state.value;
|
||||||
|
|
||||||
|
parseString(state);
|
||||||
|
option.label = state.value;
|
||||||
|
|
||||||
|
if (state.type === 'dropdown') {
|
||||||
|
parseEOT(state);
|
||||||
|
} else {
|
||||||
|
parseString(state);
|
||||||
|
}
|
||||||
|
option.value = state.value;
|
||||||
|
|
||||||
|
result.options.push(option);
|
||||||
|
}
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
result.default = result.options[0].name;
|
||||||
|
} else {
|
||||||
|
// text, color
|
||||||
|
parseStringToEnd(state);
|
||||||
|
result.default = state.value;
|
||||||
|
}
|
||||||
|
state.usercssData.vars[result.name] = result;
|
||||||
|
validVar(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOption(label, value) {
|
||||||
|
let name;
|
||||||
|
const match = label.match(/^(\w+):(.*)/);
|
||||||
|
if (match) {
|
||||||
|
([, name, label] = match);
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
name = label;
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
value = name;
|
||||||
|
}
|
||||||
|
return {name, label, value};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEOT(state) {
|
||||||
|
const re = /<<<EOT([\s\S]+?)EOT;/y;
|
||||||
|
re.lastIndex = state.re.lastIndex;
|
||||||
|
const match = state.text.match(re);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('missing EOT');
|
||||||
|
}
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
state.value = match[1].trim().replace(/\*\\\//g, '*/');
|
||||||
|
eatWhitespace(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStringUnquoted(state) {
|
||||||
|
const re = /[^"]*/y;
|
||||||
|
re.lastIndex = state.re.lastIndex;
|
||||||
|
const match = state.text.match(re);
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
state.value = match[0].trim().replace(/\s+/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseString(state) {
|
||||||
|
const match = state.text.slice(state.re.lastIndex).match(
|
||||||
|
state.text[state.re.lastIndex] === '`' ?
|
||||||
|
/^(`(?:\\`|[\s\S])*?`)\s*/ :
|
||||||
|
/^((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/
|
||||||
|
);
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
state.value = unquote(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJSONValue(state) {
|
||||||
|
const JSON_PRIME = {
|
||||||
|
__proto__: null,
|
||||||
|
'null': null,
|
||||||
|
'true': true,
|
||||||
|
'false': false
|
||||||
|
};
|
||||||
|
if (state.text[state.re.lastIndex] === '{') {
|
||||||
|
// object
|
||||||
|
const obj = {};
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
while (state.text[state.re.lastIndex] !== '}') {
|
||||||
|
parseString(state);
|
||||||
|
const key = state.value;
|
||||||
|
if (state.text[state.re.lastIndex] !== ':') {
|
||||||
|
throw new Error('missing \':\'');
|
||||||
|
}
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
parseJSONValue(state);
|
||||||
|
obj[key] = state.value;
|
||||||
|
if (state.text[state.re.lastIndex] === ',') {
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
} else if (state.text[state.re.lastIndex] !== '}') {
|
||||||
|
throw new Error('missing \',\' or \'}\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
state.value = obj;
|
||||||
|
} else if (state.text[state.re.lastIndex] === '[') {
|
||||||
|
// array
|
||||||
|
const arr = [];
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
while (state.text[state.re.lastIndex] !== ']') {
|
||||||
|
parseJSONValue(state);
|
||||||
|
arr.push(state.value);
|
||||||
|
if (state.text[state.re.lastIndex] === ',') {
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
} else if (state.text[state.re.lastIndex] !== ']') {
|
||||||
|
throw new Error('missing \',\' or \']\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.re.lastIndex++;
|
||||||
|
eatWhitespace(state);
|
||||||
|
state.value = arr;
|
||||||
|
} else if (state.text[state.re.lastIndex] === '"' || state.text[state.re.lastIndex] === '`') {
|
||||||
|
// string
|
||||||
|
parseString(state);
|
||||||
|
} else if (/\d/.test(state.text[state.re.lastIndex])) {
|
||||||
|
// number
|
||||||
|
parseNumber(state);
|
||||||
|
} else {
|
||||||
|
parseWord(state);
|
||||||
|
if (!(state.value in JSON_PRIME)) {
|
||||||
|
throw new Error(`unknown literal '${state.value}'`);
|
||||||
|
}
|
||||||
|
state.value = JSON_PRIME[state.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumber(state) {
|
||||||
|
const match = state.slice(state.re.lastIndex).match(/^-?\d+(\.\d+)?\s*/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('invalid number');
|
||||||
|
}
|
||||||
|
state.value = Number(match[0].trim());
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eatWhitespace(state) {
|
||||||
|
const match = state.text.slice(state.re.lastIndex).match(/\s*/);
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStringToEnd(state) {
|
||||||
|
const match = state.text.slice(state.re.lastIndex).match(/.+/);
|
||||||
|
state.value = unquote(match[0].trim());
|
||||||
|
state.re.lastIndex += match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unquote(s) {
|
||||||
|
const q = s[0];
|
||||||
|
if (q === s[s.length - 1] && /['"`]/.test(q)) {
|
||||||
|
// http://www.json.org/
|
||||||
|
return s.slice(1, -1).replace(
|
||||||
|
new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'),
|
||||||
|
s => {
|
||||||
|
if (s[1] === q) {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
return JSON.parse(`"${s}"`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMeta(sourceCode) {
|
||||||
|
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
||||||
|
|
||||||
|
const usercssData = {
|
||||||
|
vars: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
enabled: true,
|
||||||
|
sourceCode,
|
||||||
|
sections: [],
|
||||||
|
usercssData
|
||||||
|
};
|
||||||
|
|
||||||
|
const {text, index: metaIndex} = getMetaSource(sourceCode);
|
||||||
|
const re = /@(\w+)\s+/mg;
|
||||||
|
const state = {style, re, text, usercssData};
|
||||||
|
|
||||||
|
function doParse() {
|
||||||
|
let match;
|
||||||
|
while ((match = re.exec(text))) {
|
||||||
|
state.key = match[1];
|
||||||
|
if (!(state.key in METAS)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (state.key === 'var' || state.key === 'advanced') {
|
||||||
|
if (state.key === 'advanced') {
|
||||||
|
state.maybeUSO = true;
|
||||||
|
}
|
||||||
|
parseVar(state);
|
||||||
|
} else {
|
||||||
|
parseStringToEnd(state);
|
||||||
|
usercssData[state.key] = state.value;
|
||||||
|
}
|
||||||
|
if (state.key === 'version') {
|
||||||
|
usercssData[state.key] = normalizeVersion(usercssData[state.key]);
|
||||||
|
validVersion(usercssData[state.key]);
|
||||||
|
}
|
||||||
|
if (METAS[state.key]) {
|
||||||
|
style[state.key] = usercssData[state.key];
|
||||||
|
}
|
||||||
|
if (state.key === 'homepageURL' || state.key === 'supportURL') {
|
||||||
|
validUrl(usercssData[state.key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
doParse();
|
||||||
|
} catch (e) {
|
||||||
|
// grab additional info
|
||||||
|
e.index = metaIndex + state.re.lastIndex;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.maybeUSO && !usercssData.preprocessor) {
|
||||||
|
usercssData.preprocessor = 'uso';
|
||||||
|
}
|
||||||
|
if (usercssData.homepageURL) {
|
||||||
|
style.url = usercssData.homepageURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(style);
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVersion(version) {
|
||||||
|
// https://docs.npmjs.com/misc/semver#versions
|
||||||
|
if (version[0] === 'v' || version[0] === '=') {
|
||||||
|
return version.slice(1);
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCode(style) {
|
||||||
|
const {usercssData: {preprocessor, vars}, sourceCode} = style;
|
||||||
|
let builder;
|
||||||
|
if (preprocessor) {
|
||||||
|
if (!BUILDER[preprocessor]) {
|
||||||
|
return Promise.reject(chrome.i18n.getMessage('styleMetaErrorPreprocessor', preprocessor));
|
||||||
|
}
|
||||||
|
builder = BUILDER[preprocessor];
|
||||||
|
} else {
|
||||||
|
builder = BUILDER.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sVars = simpleVars(vars);
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
// preprocess
|
||||||
|
if (builder.preprocess) {
|
||||||
|
return builder.preprocess(sourceCode, sVars);
|
||||||
|
}
|
||||||
|
return sourceCode;
|
||||||
|
}).then(mozStyle =>
|
||||||
|
// moz-parser
|
||||||
|
loadScript('/js/moz-parser.js').then(() =>
|
||||||
|
mozParser.parse(mozStyle).then(sections => {
|
||||||
|
style.sections = sections;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
// postprocess
|
||||||
|
if (builder.postprocess) {
|
||||||
|
return builder.postprocess(style.sections, sVars);
|
||||||
|
}
|
||||||
|
}).then(() => style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function simpleVars(vars) {
|
||||||
|
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
|
||||||
|
// need to test each va's default value.
|
||||||
|
return Object.keys(vars).reduce((output, key) => {
|
||||||
|
const va = vars[key];
|
||||||
|
output[key] = Object.assign({}, va, {
|
||||||
|
value: va.value === null || va.value === undefined ?
|
||||||
|
getVarValue(va, 'default') : getVarValue(va, 'value')
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVarValue(va, prop) {
|
||||||
|
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
|
||||||
|
// TODO: handle customized image
|
||||||
|
return va.options.find(o => o.name === va[prop]).value;
|
||||||
|
}
|
||||||
|
return va[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(style) {
|
||||||
|
const {usercssData: data} = style;
|
||||||
|
// mandatory fields
|
||||||
|
for (const prop of ['name', 'namespace', 'version']) {
|
||||||
|
if (!data[prop]) {
|
||||||
|
throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// validate version
|
||||||
|
validVersion(data.version);
|
||||||
|
|
||||||
|
// validate URLs
|
||||||
|
validUrl(data.homepageURL);
|
||||||
|
validUrl(data.supportURL);
|
||||||
|
|
||||||
|
// validate vars
|
||||||
|
for (const key of Object.keys(data.vars)) {
|
||||||
|
validVar(data.vars[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validVersion(version) {
|
||||||
|
semverCompare(version, '0.0.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function validUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
url = new URL(url);
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
throw new Error(`${url.protocol} is not a valid protocol`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validVar(va, value = 'default') {
|
||||||
|
if (va.type === 'select' || va.type === 'dropdown') {
|
||||||
|
if (va.options.every(o => o.name !== va[value])) {
|
||||||
|
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectValueMismatch'));
|
||||||
|
}
|
||||||
|
} else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) {
|
||||||
|
throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox'));
|
||||||
|
} else if (va.type === 'color') {
|
||||||
|
va[value] = colorParser.format(colorParser.parse(va[value]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignVars(style, oldStyle) {
|
||||||
|
const {usercssData: {vars}} = style;
|
||||||
|
const {usercssData: {vars: oldVars}} = oldStyle;
|
||||||
|
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
||||||
|
for (const key of Object.keys(vars)) {
|
||||||
|
if (oldVars[key] && oldVars[key].value) {
|
||||||
|
vars[key].value = oldVars[key].value;
|
||||||
|
try {
|
||||||
|
validVar(vars[key], 'value');
|
||||||
|
} catch (e) {
|
||||||
|
vars[key].value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {buildMeta, buildCode, assignVars};
|
||||||
|
})();
|
13
manage.html
13
manage.html
|
@ -5,6 +5,7 @@
|
||||||
<title i18n-text="manageTitle"></title>
|
<title i18n-text="manageTitle"></title>
|
||||||
<link rel="stylesheet" href="manage/manage.css">
|
<link rel="stylesheet" href="manage/manage.css">
|
||||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
<link rel="stylesheet" href="msgbox/msgbox.css">
|
||||||
|
<link rel="stylesheet" href="options/onoffswitch.css">
|
||||||
|
|
||||||
<style id="style-overrides"></style>
|
<style id="style-overrides"></style>
|
||||||
|
|
||||||
|
@ -83,6 +84,16 @@
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template data-id="configureIcon">
|
||||||
|
<span class="configure-usercss" i18n-title="configureStyle">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
d="M 10,2.0423828 A 7.9575898,7.9575898 0 0 0 8.8908203,2.1285156 V 4.355664 A 5.7578608,5.7578608 0 0 0 6.7919922,5.2240235 l -1.575,-1.575 A 7.9575898,7.9575898 0 0 0 3.6507813,5.21875 L 5.2222656,6.7902344 A 5.7578608,5.7578608 0 0 0 4.3521485,8.8908203 H 2.1302735 A 7.9575898,7.9575898 0 0 0 2.0423828,10 7.9575898,7.9575898 0 0 0 2.1285156,11.10918 H 4.355664 a 5.7578608,5.7578608 0 0 0 0.8683595,2.098828 l -1.575,1.575 A 7.9575898,7.9575898 0 0 0 5.21875,16.349219 l 1.5714844,-1.571484 a 5.7578608,5.7578608 0 0 0 2.1005859,0.870117 v 2.221875 A 7.9575898,7.9575898 0 0 0 10,17.957617 a 7.9575898,7.9575898 0 0 0 1.10918,-0.08613 v -2.227149 a 5.7578608,5.7578608 0 0 0 2.098828,-0.868359 l 1.575,1.575 a 7.9575898,7.9575898 0 0 0 1.566211,-1.569727 l -1.571484,-1.571485 a 5.7578608,5.7578608 0 0 0 0.870117,-2.100585 h 2.221875 A 7.9575898,7.9575898 0 0 0 17.957617,10 7.9575898,7.9575898 0 0 0 17.871485,8.8908203 H 15.644336 A 5.7578608,5.7578608 0 0 0 14.775977,6.7919922 l 1.575,-1.575 A 7.9575898,7.9575898 0 0 0 14.78125,3.6507813 L 13.209765,5.2222656 A 5.7578608,5.7578608 0 0 0 11.10918,4.3521485 V 2.1302735 A 7.9575898,7.9575898 0 0 0 10,2.0423828 Z m 0,4.2574219 A 3.6994645,3.6994645 0 0 1 13.700195,10 3.6994645,3.6994645 0 0 1 10,13.700195 3.6994645,3.6994645 0 0 1 6.2998047,10 3.6994645,3.6994645 0 0 1 10,6.2998047 Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template data-id="updaterIcons">
|
<template data-id="updaterIcons">
|
||||||
<span class="updater-icons">
|
<span class="updater-icons">
|
||||||
<span class="check-update" i18n-title="checkForUpdate">
|
<span class="check-update" i18n-title="checkForUpdate">
|
||||||
|
@ -139,6 +150,8 @@
|
||||||
<script src="manage/filters.js"></script>
|
<script src="manage/filters.js"></script>
|
||||||
<script src="manage/updater-ui.js"></script>
|
<script src="manage/updater-ui.js"></script>
|
||||||
<script src="manage/object-diff.js"></script>
|
<script src="manage/object-diff.js"></script>
|
||||||
|
<script src="js/color-parser.js"></script>
|
||||||
|
<script src="manage/config-dialog.js"></script>
|
||||||
<script src="manage/manage.js"></script>
|
<script src="manage/manage.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
156
manage/config-dialog.js
Normal file
156
manage/config-dialog.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
/* global colorParser messageBox makeLink */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function configDialog(style) {
|
||||||
|
const form = buildConfigForm();
|
||||||
|
|
||||||
|
return messageBox({
|
||||||
|
title: `${style.name} v${style.usercssData.version}`,
|
||||||
|
className: 'config-dialog',
|
||||||
|
contents: [
|
||||||
|
$element({
|
||||||
|
className: 'config-heading',
|
||||||
|
appendChild: style.usercssData.supportURL && makeLink({
|
||||||
|
className: 'external-support',
|
||||||
|
href: style.usercssData.supportURL,
|
||||||
|
textContent: t('externalFeedback')
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
$element({
|
||||||
|
className: 'config-body',
|
||||||
|
appendChild: form.elements
|
||||||
|
})
|
||||||
|
],
|
||||||
|
buttons: [
|
||||||
|
t('confirmSave'),
|
||||||
|
{
|
||||||
|
textContent: t('confirmDefault'),
|
||||||
|
onclick: form.useDefault
|
||||||
|
},
|
||||||
|
t('confirmCancel')
|
||||||
|
]
|
||||||
|
}).then(result => {
|
||||||
|
if (result.button !== 0 && !result.enter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return form.getVars();
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildConfigForm() {
|
||||||
|
const labels = [];
|
||||||
|
const vars = deepCopy(style.usercssData.vars);
|
||||||
|
for (const key of Object.keys(vars)) {
|
||||||
|
const va = vars[key];
|
||||||
|
let appendChild;
|
||||||
|
switch (va.type) {
|
||||||
|
case 'color':
|
||||||
|
va.inputColor = $element({tag: 'input', type: 'color'});
|
||||||
|
va.inputAlpha = $element({
|
||||||
|
tag: 'input',
|
||||||
|
type: 'range',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
title: chrome.i18n.getMessage('alphaChannel'),
|
||||||
|
step: 'any'
|
||||||
|
});
|
||||||
|
va.inputColor.onchange = va.inputAlpha.oninput = () => {
|
||||||
|
va.dirty = true;
|
||||||
|
const color = colorParser.parse(va.inputColor.value);
|
||||||
|
color.a = Number(va.inputAlpha.value);
|
||||||
|
va.value = colorParser.format(color);
|
||||||
|
va.inputColor.style.opacity = color.a;
|
||||||
|
};
|
||||||
|
appendChild = [
|
||||||
|
$element({appendChild: [va.inputColor, va.inputAlpha]})
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
va.input = $element({tag: 'input', type: 'checkbox'});
|
||||||
|
va.input.onchange = () => {
|
||||||
|
va.dirty = true;
|
||||||
|
va.value = String(Number(va.input.checked));
|
||||||
|
};
|
||||||
|
appendChild = [
|
||||||
|
$element({tag: 'span', className: 'onoffswitch', appendChild: [
|
||||||
|
va.input,
|
||||||
|
$element({tag: 'span'})
|
||||||
|
]})
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
case 'dropdown':
|
||||||
|
case 'image':
|
||||||
|
// TODO: a image picker input?
|
||||||
|
va.input = $element({
|
||||||
|
tag: 'select',
|
||||||
|
appendChild: va.options.map(o => $element({
|
||||||
|
tag: 'option', value: o.name, textContent: o.label
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
va.input.onchange = () => {
|
||||||
|
va.dirty = true;
|
||||||
|
va.value = va.input.value;
|
||||||
|
};
|
||||||
|
appendChild = [va.input];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
va.input = $element({tag: 'input', type: 'text'});
|
||||||
|
va.input.oninput = () => {
|
||||||
|
va.dirty = true;
|
||||||
|
va.value = va.input.value;
|
||||||
|
};
|
||||||
|
appendChild = [va.input];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
appendChild.unshift($element({tag: 'span', appendChild: va.label}));
|
||||||
|
labels.push($element({
|
||||||
|
tag: 'label',
|
||||||
|
className: `config-${va.type}`,
|
||||||
|
appendChild
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
drawValues();
|
||||||
|
|
||||||
|
function drawValues() {
|
||||||
|
for (const key of Object.keys(vars)) {
|
||||||
|
const va = vars[key];
|
||||||
|
const value = va.value === null || va.value === undefined ?
|
||||||
|
va.default : va.value;
|
||||||
|
|
||||||
|
if (va.type === 'color') {
|
||||||
|
const color = colorParser.parse(value);
|
||||||
|
va.inputAlpha.value = color.a;
|
||||||
|
va.inputColor.style.opacity = color.a;
|
||||||
|
delete color.a;
|
||||||
|
va.inputColor.value = colorParser.formatHex(color);
|
||||||
|
} else if (va.type === 'checkbox') {
|
||||||
|
va.input.checked = Number(value);
|
||||||
|
} else {
|
||||||
|
va.input.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDefault() {
|
||||||
|
for (const key of Object.keys(vars)) {
|
||||||
|
const va = vars[key];
|
||||||
|
va.dirty = va.value !== null && va.value !== undefined && va.value !== va.default;
|
||||||
|
va.value = null;
|
||||||
|
}
|
||||||
|
drawValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVars() {
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: labels,
|
||||||
|
useDefault,
|
||||||
|
getVars
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -652,6 +652,79 @@ fieldset > *:not(legend) {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* config dialog */
|
||||||
|
.config-dialog .config-heading {
|
||||||
|
float: right;
|
||||||
|
margin: -1.25rem 0 0 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label {
|
||||||
|
display: flex;
|
||||||
|
padding: .75em 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label:not(:first-child) {
|
||||||
|
border-top: 1px dotted #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label > :first-child {
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label:not([disabled]) > :first-child {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label:not([disabled]):hover > :first-child {
|
||||||
|
text-shadow: 0 0 0.01px rgba(0, 0, 0, .25);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog input,
|
||||||
|
.config-dialog select,
|
||||||
|
.config-dialog .onoffswitch {
|
||||||
|
width: 60px;
|
||||||
|
margin: 0;
|
||||||
|
height: 2em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog select {
|
||||||
|
width: auto;
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 124px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog .onoffswitch {
|
||||||
|
height: auto;
|
||||||
|
margin: calc((2em - 12px) / 2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog input[type="text"] {
|
||||||
|
padding-left: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label > :last-child {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-dialog label > :last-child:not(.onoffswitch) > :not(:last-child) {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
/* global filtersSelector, filterAndAppend */
|
/* global filtersSelector, filterAndAppend */
|
||||||
/* global checkUpdate, handleUpdateInstalled */
|
/* global checkUpdate, handleUpdateInstalled */
|
||||||
/* global objectDiff */
|
/* global objectDiff */
|
||||||
|
/* global configDialog */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let installed;
|
let installed;
|
||||||
|
@ -192,12 +193,19 @@ function createStyleElement({style, name}) {
|
||||||
if (style.updateUrl && newUI.enabled) {
|
if (style.updateUrl && newUI.enabled) {
|
||||||
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
|
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
|
||||||
}
|
}
|
||||||
|
if (shouldShowConfig() && newUI.enabled) {
|
||||||
|
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
// name being supplied signifies we're invoked by showStyles()
|
// name being supplied signifies we're invoked by showStyles()
|
||||||
// which debounces its main loop thus loading the postponed favicons
|
// which debounces its main loop thus loading the postponed favicons
|
||||||
createStyleTargetsElement({entry, style, postponeFavicons: name});
|
createStyleTargetsElement({entry, style, postponeFavicons: name});
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
|
|
||||||
|
function shouldShowConfig() {
|
||||||
|
return style.usercssData && Object.keys(style.usercssData.vars).length > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -275,6 +283,25 @@ Object.assign(handleEvent, {
|
||||||
'.update': 'update',
|
'.update': 'update',
|
||||||
'.delete': 'delete',
|
'.delete': 'delete',
|
||||||
'.applies-to .expander': 'expandTargets',
|
'.applies-to .expander': 'expandTargets',
|
||||||
|
'.configure-usercss': 'config'
|
||||||
|
},
|
||||||
|
|
||||||
|
config(event, {styleMeta: style}) {
|
||||||
|
configDialog(style).then(vars => {
|
||||||
|
if (!vars) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keys = Object.keys(vars).filter(k => vars[k].dirty);
|
||||||
|
if (!keys.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
style.reason = 'config';
|
||||||
|
for (const key of keys) {
|
||||||
|
style.usercssData.vars[key].value = vars[key].value;
|
||||||
|
}
|
||||||
|
onBackgroundReady()
|
||||||
|
.then(() => BG.usercssHelper.save(style));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
entryClicked(event) {
|
entryClicked(event) {
|
||||||
|
@ -331,12 +358,18 @@ Object.assign(handleEvent, {
|
||||||
},
|
},
|
||||||
|
|
||||||
update(event, entry) {
|
update(event, entry) {
|
||||||
// update everything but name
|
const request = Object.assign(entry.updatedCode, {
|
||||||
saveStyleSafe(Object.assign(entry.updatedCode, {
|
|
||||||
id: entry.styleId,
|
id: entry.styleId,
|
||||||
name: null,
|
|
||||||
reason: 'update',
|
reason: 'update',
|
||||||
}));
|
});
|
||||||
|
if (entry.updatedCode.usercssData) {
|
||||||
|
onBackgroundReady()
|
||||||
|
.then(() => BG.usercssHelper.save(request));
|
||||||
|
} else {
|
||||||
|
// update everything but name
|
||||||
|
request.name = null;
|
||||||
|
saveStyleSafe(request);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(event, entry) {
|
delete(event, entry) {
|
||||||
|
|
|
@ -114,7 +114,11 @@ function reportUpdateState(state, style, details) {
|
||||||
if (entry.classList.contains('can-update')) {
|
if (entry.classList.contains('can-update')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const same = details === BG.updater.SAME_MD5 || details === BG.updater.SAME_CODE;
|
const same = (
|
||||||
|
details === BG.updater.SAME_MD5 ||
|
||||||
|
details === BG.updater.SAME_CODE ||
|
||||||
|
details === BG.updater.SAME_VERSION
|
||||||
|
);
|
||||||
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
|
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
|
||||||
entry.dataset.details = details;
|
entry.dataset.details = details;
|
||||||
if (!details) {
|
if (!details) {
|
||||||
|
|
|
@ -22,9 +22,14 @@
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"js/messaging.js",
|
"js/messaging.js",
|
||||||
"vendor-overwrites/lz-string/LZString-2xspeedup.js",
|
"vendor-overwrites/lz-string/LZString-2xspeedup.js",
|
||||||
|
"js/color-parser.js",
|
||||||
|
"js/usercss.js",
|
||||||
"background/storage.js",
|
"background/storage.js",
|
||||||
|
"background/usercss-helper.js",
|
||||||
"js/prefs.js",
|
"js/prefs.js",
|
||||||
|
"js/script-loader.js",
|
||||||
"background/background.js",
|
"background/background.js",
|
||||||
|
"vendor/node-semver/semver.js",
|
||||||
"background/update.js"
|
"background/update.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -49,6 +54,13 @@
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": false,
|
"all_frames": false,
|
||||||
"js": ["content/install.js"]
|
"js": ["content/install.js"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"include_globs": ["*.user.css", "*.user.styl"],
|
||||||
|
"run_at": "document_idle",
|
||||||
|
"all_frames": false,
|
||||||
|
"js": ["content/util.js", "content/install-user-css.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
|
|
|
@ -40,17 +40,27 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message-box.center #message-box-contents pre {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
#message-box.center > div {
|
#message-box.center > div {
|
||||||
top: unset;
|
top: unset;
|
||||||
right: unset;
|
right: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message-box.pre #message-box-contents {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
#message-box-title {
|
#message-box-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: rgb(145, 208, 198);
|
background-color: rgb(145, 208, 198);
|
||||||
padding: .75rem 24px .75rem 52px;
|
padding: .75rem 24px .75rem 52px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 42px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-box-title::before {
|
#message-box-title::before {
|
||||||
|
|
|
@ -4,7 +4,7 @@ function messageBox({
|
||||||
title, // [mandatory] string
|
title, // [mandatory] string
|
||||||
contents, // [mandatory] 1) DOM element 2) string
|
contents, // [mandatory] 1) DOM element 2) string
|
||||||
className = '', // string, CSS class name of the message box element
|
className = '', // string, CSS class name of the message box element
|
||||||
buttons = [], // array of strings used as labels
|
buttons = [], // array of strings or objects like {textContent[string], onclick[function]}.
|
||||||
onshow, // function(messageboxElement) invoked after the messagebox is shown
|
onshow, // function(messageboxElement) invoked after the messagebox is shown
|
||||||
blockScroll, // boolean, blocks the page scroll
|
blockScroll, // boolean, blocks the page scroll
|
||||||
}) { // RETURNS: Promise resolved to {button[number], enter[boolean], esc[boolean]}
|
}) { // RETURNS: Promise resolved to {button[number], enter[boolean], esc[boolean]}
|
||||||
|
@ -69,14 +69,12 @@ function messageBox({
|
||||||
onclick: messageBox.listeners.closeIcon}),
|
onclick: messageBox.listeners.closeIcon}),
|
||||||
$element({id: `${id}-contents`, appendChild: tHTML(contents)}),
|
$element({id: `${id}-contents`, appendChild: tHTML(contents)}),
|
||||||
$element({id: `${id}-buttons`, appendChild:
|
$element({id: `${id}-buttons`, appendChild:
|
||||||
buttons.map((textContent, buttonIndex) => textContent &&
|
buttons.map((content, buttonIndex) => content && $element({
|
||||||
$element({
|
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
buttonIndex,
|
buttonIndex,
|
||||||
textContent,
|
textContent: content.textContent || content,
|
||||||
onclick: messageBox.listeners.button,
|
onclick: content.onclick || messageBox.listeners.button,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
]}),
|
]}),
|
||||||
]});
|
]});
|
||||||
|
@ -101,3 +99,17 @@ function messageBox({
|
||||||
messageBox.resolve = null;
|
messageBox.resolve = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageBox.alert = text =>
|
||||||
|
messageBox({
|
||||||
|
contents: text,
|
||||||
|
className: 'pre center',
|
||||||
|
buttons: [t('confirmClose')]
|
||||||
|
});
|
||||||
|
|
||||||
|
messageBox.confirm = text =>
|
||||||
|
messageBox({
|
||||||
|
contents: text,
|
||||||
|
className: 'pre center',
|
||||||
|
buttons: [t('confirmYes'), t('confirmNo')]
|
||||||
|
}).then(result => result.button === 0 || result.enter);
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title i18n-text-append="optionsHeading">Stylus </title>
|
<title i18n-text-append="optionsHeading">Stylus </title>
|
||||||
<link rel="stylesheet" href="options/options.css">
|
<link rel="stylesheet" href="options/options.css">
|
||||||
|
<link rel="stylesheet" href="options/onoffswitch.css">
|
||||||
|
|
||||||
<style id="firefox-transitions-bug-suppressor">
|
<style id="firefox-transitions-bug-suppressor">
|
||||||
/* restrict to FF */
|
/* restrict to FF */
|
||||||
|
@ -122,6 +123,13 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span i18n-text="optionsAdvancedNewStyleAsUsercss"></span>
|
||||||
|
<span class="onoffswitch">
|
||||||
|
<input type="checkbox" id="newStyleAsUsercss">
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
59
options/onoffswitch.css
Normal file
59
options/onoffswitch.css
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/* 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;
|
||||||
|
height: 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);
|
||||||
|
}
|
|
@ -96,6 +96,7 @@ label:not([disabled]):hover > :first-child {
|
||||||
button,
|
button,
|
||||||
input[type=number],
|
input[type=number],
|
||||||
input[type="color"],
|
input[type="color"],
|
||||||
|
select,
|
||||||
.onoffswitch {
|
.onoffswitch {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -221,63 +222,3 @@ sup {
|
||||||
25% { opacity: 1 }
|
25% { opacity: 1 }
|
||||||
100% { opacity: 0 }
|
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;
|
|
||||||
height: 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);
|
|
||||||
}
|
|
||||||
|
|
64
vendor/codemirror/addon/mode/loadmode.js
vendored
Normal file
64
vendor/codemirror/addon/mode/loadmode.js
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"), "cjs");
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], function(CM) { mod(CM, "amd"); });
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror, "plain");
|
||||||
|
})(function(CodeMirror, env) {
|
||||||
|
if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js";
|
||||||
|
|
||||||
|
var loading = {};
|
||||||
|
function splitCallback(cont, n) {
|
||||||
|
var countDown = n;
|
||||||
|
return function() { if (--countDown == 0) cont(); };
|
||||||
|
}
|
||||||
|
function ensureDeps(mode, cont) {
|
||||||
|
var deps = CodeMirror.modes[mode].dependencies;
|
||||||
|
if (!deps) return cont();
|
||||||
|
var missing = [];
|
||||||
|
for (var i = 0; i < deps.length; ++i) {
|
||||||
|
if (!CodeMirror.modes.hasOwnProperty(deps[i]))
|
||||||
|
missing.push(deps[i]);
|
||||||
|
}
|
||||||
|
if (!missing.length) return cont();
|
||||||
|
var split = splitCallback(cont, missing.length);
|
||||||
|
for (var i = 0; i < missing.length; ++i)
|
||||||
|
CodeMirror.requireMode(missing[i], split);
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.requireMode = function(mode, cont) {
|
||||||
|
if (typeof mode != "string") mode = mode.name;
|
||||||
|
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont);
|
||||||
|
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont);
|
||||||
|
|
||||||
|
var file = CodeMirror.modeURL.replace(/%N/g, mode);
|
||||||
|
if (env == "plain") {
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.src = file;
|
||||||
|
var others = document.getElementsByTagName("script")[0];
|
||||||
|
var list = loading[mode] = [cont];
|
||||||
|
CodeMirror.on(script, "load", function() {
|
||||||
|
ensureDeps(mode, function() {
|
||||||
|
for (var i = 0; i < list.length; ++i) list[i]();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
others.parentNode.insertBefore(script, others);
|
||||||
|
} else if (env == "cjs") {
|
||||||
|
require(file);
|
||||||
|
cont();
|
||||||
|
} else if (env == "amd") {
|
||||||
|
requirejs([file], cont);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CodeMirror.autoLoadMode = function(instance, mode) {
|
||||||
|
if (!CodeMirror.modes.hasOwnProperty(mode))
|
||||||
|
CodeMirror.requireMode(mode, function() {
|
||||||
|
instance.setOption("mode", instance.getOption("mode"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
771
vendor/codemirror/mode/stylus/stylus.js
vendored
Normal file
771
vendor/codemirror/mode/stylus/stylus.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/node-semver/README.md
vendored
Normal file
1
vendor/node-semver/README.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
See https://github.com/eight04/node-semver-bundle.
|
1
vendor/node-semver/semver.js
vendored
Normal file
1
vendor/node-semver/semver.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/stylus-lang/README.md
vendored
Normal file
1
vendor/stylus-lang/README.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
The content of this folder belongs to [stylus preprocessor](https://github.com/stylus/stylus/).
|
6
vendor/stylus-lang/stylus.min.js
vendored
Normal file
6
vendor/stylus-lang/stylus.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user