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",
|
||||
"description": "Title of the page for adding styles"
|
||||
},
|
||||
"alphaChannel": {
|
||||
"message": "Opacity",
|
||||
"description": "Label of color's opacity"
|
||||
},
|
||||
"appliesAdd": {
|
||||
"message": "Add",
|
||||
"description": "Label for the button to add an 'applies' entry"
|
||||
|
@ -36,6 +40,14 @@
|
|||
"message": "Applies to",
|
||||
"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": {
|
||||
"message": "URLs matching the regexp",
|
||||
"description": "Option to make the style apply to the entered string as a regular expression"
|
||||
|
@ -44,6 +56,10 @@
|
|||
"message": "Remove",
|
||||
"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": {
|
||||
"message": "Specify",
|
||||
"description": "Label for the button to make a style apply only to specific sites"
|
||||
|
@ -64,6 +80,10 @@
|
|||
"message": "Apply all updates",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"author": {
|
||||
"message": "Author",
|
||||
"description": "Label for the style author"
|
||||
},
|
||||
"backupButtons": {
|
||||
"message": "Backup",
|
||||
"description": "Heading for backup"
|
||||
|
@ -83,6 +103,10 @@
|
|||
"updateCheckHistory": {
|
||||
"message": "History of update checks"
|
||||
},
|
||||
"configureStyle": {
|
||||
"message": "Configure",
|
||||
"description": "Label for the button to configure userstyle"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Check for update",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
|
@ -167,6 +191,14 @@
|
|||
"message": "No",
|
||||
"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": {
|
||||
"message": "Stop",
|
||||
"description": "'Stop' button in a confirm dialog"
|
||||
|
@ -175,6 +207,10 @@
|
|||
"message": "Yes",
|
||||
"description": "'Yes' button in a confirm dialog"
|
||||
},
|
||||
"confirmClose": {
|
||||
"message": "Close",
|
||||
"description": "'Close' button in a confirm dialog"
|
||||
},
|
||||
"dbError": {
|
||||
"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"
|
||||
|
@ -257,6 +293,26 @@
|
|||
"message": "Export",
|
||||
"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": {
|
||||
"message": "$numShown$ shown of $numTotal$ total",
|
||||
"description": "TL note - make this message short",
|
||||
|
@ -345,10 +401,43 @@
|
|||
"message": "Discard contents of current style and overwrite it with the imported 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": {
|
||||
"message": "Install update",
|
||||
"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": {
|
||||
"message": "Set $linter$ rules configuration",
|
||||
"description": "Stylelint or CSSLint popup header",
|
||||
|
@ -366,6 +455,15 @@
|
|||
"message": "(Set rule as: 0 = disabled; 1 = warning; 2 = error)",
|
||||
"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": {
|
||||
"message": "Not saved due to these invalid configuration settings:",
|
||||
"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",
|
||||
"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": {
|
||||
"message": "Filters",
|
||||
"description": "Label for filters container"
|
||||
|
@ -483,6 +589,10 @@
|
|||
"message": "More Options",
|
||||
"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": {
|
||||
"message": "Shift-click or right-click opens manager with styles applicable for current site",
|
||||
"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": {
|
||||
"message": "Enter a name.",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
|
@ -645,6 +814,10 @@
|
|||
"message": "Mozilla Format",
|
||||
"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": {
|
||||
"message": "Paste the Mozilla-format code",
|
||||
"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": {
|
||||
"message": "Stylus doesn't work on pages like this.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
|
@ -743,6 +920,10 @@
|
|||
"message": "Updates installed:",
|
||||
"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": {
|
||||
"message": "Write style for: ",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
|
@ -805,6 +986,9 @@
|
|||
"optionsAdvancedContextDelete": {
|
||||
"message": "Add 'Delete' in editor context menu"
|
||||
},
|
||||
"optionsAdvancedNewStyleAsUsercss": {
|
||||
"message": "Write new style as usercss"
|
||||
},
|
||||
"optionsActions": {
|
||||
"message": "Actions"
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global dbExec, getStyles, saveStyle */
|
||||
/* global handleCssTransitionBug */
|
||||
/* global usercssHelper openEditor */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -302,6 +303,14 @@ function onRuntimeMessage(request, sender, sendResponse) {
|
|||
saveStyle(request).then(sendResponse);
|
||||
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':
|
||||
dbExec()
|
||||
.then(() => sendResponse(true))
|
||||
|
@ -313,5 +322,36 @@ function onRuntimeMessage(request, sender, sendResponse) {
|
|||
.then(sendResponse)
|
||||
.catch(() => sendResponse(null));
|
||||
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 codeIsUpdated;
|
||||
|
||||
return maybeCalcDigest()
|
||||
.then(maybeImportFix)
|
||||
.then(decide);
|
||||
|
||||
function maybeCalcDigest() {
|
||||
if (reason === 'update' || reason === 'update-digest') {
|
||||
return calcStyleDigest(style).then(digest => {
|
||||
style.originalDigest = digest;
|
||||
return decide();
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function maybeImportFix() {
|
||||
if (reason === 'import') {
|
||||
style.originalDigest = style.originalDigest || 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;
|
||||
}
|
||||
}
|
||||
return decide();
|
||||
}
|
||||
|
||||
function decide() {
|
||||
if (id !== null) {
|
||||
|
@ -714,7 +723,8 @@ function normalizeStyleSections({sections}) {
|
|||
|
||||
|
||||
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);
|
||||
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||
/* global calcStyleDigest */
|
||||
/* global usercss semverCompare usercssHelper */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -15,8 +16,10 @@ var updater = {
|
|||
MAYBE_EDITED: 'may be locally edited',
|
||||
SAME_MD5: 'up-to-date: MD5 is 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_JSON: 'error: JSON is invalid',
|
||||
ERROR_VERSION: 'error: version is older than installed style',
|
||||
|
||||
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.
|
||||
*/
|
||||
const maybeUpdate = style.usercssData ? maybeUpdateUsercss : maybeUpdateUSO;
|
||||
return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
|
||||
.then(maybeFetchMd5)
|
||||
.then(maybeFetchCode)
|
||||
.then(checkIfEdited)
|
||||
.then(maybeUpdate)
|
||||
.then(maybeValidate)
|
||||
.then(maybeSave)
|
||||
.then(saved => {
|
||||
observer(updater.UPDATED, saved);
|
||||
|
@ -67,42 +72,79 @@ var updater = {
|
|||
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
||||
});
|
||||
|
||||
function maybeFetchMd5(digest) {
|
||||
if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) {
|
||||
function checkIfEdited(digest) {
|
||||
if (ignoreDigest) {
|
||||
return;
|
||||
}
|
||||
if (style.originalDigest && style.originalDigest !== digest) {
|
||||
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) {
|
||||
return Promise.reject(updater.ERROR_MD5);
|
||||
}
|
||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.SAME_MD5);
|
||||
}
|
||||
return download(style.updateUrl);
|
||||
return download(style.updateUrl)
|
||||
.then(text => tryJSONparse(text));
|
||||
});
|
||||
}
|
||||
|
||||
function maybeSave(text) {
|
||||
const json = tryJSONparse(text);
|
||||
function maybeUpdateUsercss() {
|
||||
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)) {
|
||||
return Promise.reject(updater.ERROR_JSON);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function maybeSave(json) {
|
||||
json.id = style.id;
|
||||
if (styleSectionsEqual(json, style)) {
|
||||
// JSONs may have different order of items even if sections are effectively equal
|
||||
// so we'll update the digest anyway
|
||||
// always update digest even if (save === false)
|
||||
saveStyle(Object.assign(json, {reason: 'update-digest'}));
|
||||
return Promise.reject(updater.SAME_CODE);
|
||||
} else if (!style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.MAYBE_EDITED);
|
||||
}
|
||||
return !save ? json :
|
||||
saveStyle(Object.assign(json, {
|
||||
name: null, // keep local name customizations
|
||||
reason: 'update',
|
||||
}));
|
||||
if (!save) {
|
||||
return json;
|
||||
}
|
||||
json.reason = 'update';
|
||||
if (json.usercssData) {
|
||||
return usercssHelper.save(json);
|
||||
}
|
||||
json.name = null; // keep local name customizations
|
||||
return saveStyle(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/prefs.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>
|
||||
<link rel="stylesheet" href="edit/edit.css">
|
||||
<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="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/css-hint.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/addon/mode/loadmode.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
||||
|
||||
<script src="/edit/codemirror-default.js"></script>
|
||||
<link rel="stylesheet" href="/edit/codemirror-default.css">
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<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: "";
|
||||
opacity: .15;
|
||||
}
|
||||
/* footer */
|
||||
#footer {
|
||||
margin-top: 1em;
|
||||
}
|
||||
/************ content ***********/
|
||||
#sections > div {
|
||||
margin: 0.7rem;
|
||||
|
@ -174,18 +178,11 @@ h2 .svg-icon, label .svg-icon {
|
|||
margin-left: 0.25rem;
|
||||
}
|
||||
/* code */
|
||||
.CodeMirror-hint:hover {
|
||||
color: white;
|
||||
background: #08f;
|
||||
}
|
||||
.code {
|
||||
height: 10rem;
|
||||
width: 40rem;
|
||||
}
|
||||
.CodeMirror {
|
||||
border: solid #CCC 1px;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
.resize-grip-enabled .CodeMirror-scroll {
|
||||
height: auto !important;;
|
||||
position: absolute !important;
|
||||
top: 0;
|
||||
|
@ -193,34 +190,15 @@ h2 .svg-icon, label .svg-icon {
|
|||
right: 0;
|
||||
bottom: 6px; /* resize-grip height */
|
||||
}
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
.resize-grip-enabled .CodeMirror-vscrollbar {
|
||||
margin-bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
.resize-grip-enabled .CodeMirror-hscrollbar {
|
||||
bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
.resize-grip-enabled .CodeMirror-scrollbar-filler {
|
||||
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"] .CodeMirror-selection-highlight-scrollbar {
|
||||
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);
|
||||
}
|
||||
|
||||
/************ 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 ************/
|
||||
@media(max-width:737px) {
|
||||
#header {
|
||||
|
|
627
edit/edit.js
627
edit/edit.js
|
@ -1,8 +1,10 @@
|
|||
/* eslint brace-style: 0, operator-linebreak: 0 */
|
||||
/* global CodeMirror parserlib */
|
||||
/* global onDOMscripted */
|
||||
/* global loadScript */
|
||||
/* global css_beautify */
|
||||
/* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
|
||||
/* global mozParser createSourceEditor */
|
||||
/* global closeCurrentTab regExpTester messageBox */
|
||||
'use strict';
|
||||
|
||||
let styleId = null;
|
||||
|
@ -18,6 +20,8 @@ let useHistoryBack;
|
|||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||
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
|
||||
onBackgroundReady();
|
||||
|
||||
|
@ -160,111 +164,16 @@ function setCleanSection(section) {
|
|||
|
||||
function initCodeMirror() {
|
||||
const CM = CodeMirror;
|
||||
const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0;
|
||||
// lint.js is not loaded initially
|
||||
// CodeMirror miserably fails on keyMap='' so let's ensure it's not
|
||||
if (!prefs.get('editor.keyMap')) {
|
||||
prefs.reset('editor.keyMap');
|
||||
}
|
||||
|
||||
// default option values
|
||||
Object.assign(CM.defaults, {
|
||||
mode: 'css',
|
||||
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'));
|
||||
CM.defaults.lint = linterConfig.getForCodeMirror();
|
||||
|
||||
// additional commands
|
||||
CM.commands.jumpToLine = jumpToLine;
|
||||
CM.commands.nextEditor = cm => nextPrevEditor(cm, 1);
|
||||
CM.commands.prevEditor = cm => nextPrevEditor(cm, -1);
|
||||
CM.commands.save = save;
|
||||
CM.commands.blockComment = cm => {
|
||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||
};
|
||||
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
|
||||
CM.getOption = o => CodeMirror.defaults[o];
|
||||
CM.setOption = (o, v) => {
|
||||
|
@ -434,11 +343,7 @@ function acmeEventListener(event) {
|
|||
return;
|
||||
}
|
||||
case 'autocompleteOnTyping':
|
||||
editors.forEach(cm => {
|
||||
const onOff = el.checked ? 'on' : 'off';
|
||||
cm[onOff]('changes', autocompleteOnTyping);
|
||||
cm[onOff]('pick', autocompletePicked);
|
||||
});
|
||||
editors.forEach(cm => setupAutocomplete(cm, el.checked));
|
||||
return;
|
||||
case 'matchHighlight':
|
||||
switch (value) {
|
||||
|
@ -463,8 +368,7 @@ function setupCodeMirror(textarea, index) {
|
|||
|
||||
cm.on('changes', indicateCodeChangeDebounced);
|
||||
if (prefs.get('editor.autocompleteOnTyping')) {
|
||||
cm.on('changes', autocompleteOnTyping);
|
||||
cm.on('pick', autocompletePicked);
|
||||
setupAutocomplete(cm);
|
||||
}
|
||||
wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
|
||||
cm.on('blur', () => {
|
||||
|
@ -504,6 +408,7 @@ function setupCodeMirror(textarea, index) {
|
|||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||
}
|
||||
|
||||
wrapper.classList.add('resize-grip-enabled');
|
||||
let lastClickTime = 0;
|
||||
const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
|
||||
resizeGrip.onmousedown = event => {
|
||||
|
@ -671,12 +576,20 @@ window.onbeforeunload = () => {
|
|||
rememberWindowSize();
|
||||
}
|
||||
document.activeElement.blur();
|
||||
if (isCleanGlobal()) {
|
||||
if (isClean()) {
|
||||
return;
|
||||
}
|
||||
updateLintReportIfEnabled(null, 0);
|
||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||
return t('styleChangesNotSaved');
|
||||
|
||||
function isClean() {
|
||||
if (editor) {
|
||||
return !editor.isDirty();
|
||||
} else {
|
||||
return isCleanGlobal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function addAppliesTo(list, name, value) {
|
||||
|
@ -737,20 +650,30 @@ function addSection(event, section) {
|
|||
|
||||
toggleTestRegExpVisibility();
|
||||
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
|
||||
$('.test-regexp', div).onclick = showRegExpTester;
|
||||
function toggleTestRegExpVisibility() {
|
||||
const show = [...appliesTo.children].some(item =>
|
||||
$('.test-regexp', div).onclick = () => {
|
||||
regExpTester.toggle();
|
||||
regExpTester.update(getRegExps());
|
||||
};
|
||||
|
||||
function getRegExps() {
|
||||
return [...appliesTo.children]
|
||||
.map(item =>
|
||||
!item.matches('.applies-to-everything') &&
|
||||
$('.applies-type', item).value === 'regexp' &&
|
||||
$('.applies-value', item).value.trim()
|
||||
);
|
||||
)
|
||||
.filter(item => item);
|
||||
}
|
||||
|
||||
function toggleTestRegExpVisibility() {
|
||||
const show = getRegExps().length > 0;
|
||||
div.classList.toggle('has-regexp', show);
|
||||
appliesTo.oninput = appliesTo.oninput || show && (event => {
|
||||
if (
|
||||
event.target.matches('.applies-value') &&
|
||||
$('.applies-type', event.target.parentElement).value === 'regexp'
|
||||
) {
|
||||
showRegExpTester(null, div);
|
||||
regExpTester.update(getRegExps());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1075,6 +998,14 @@ function jumpToLine(cm) {
|
|||
}
|
||||
|
||||
function toggleStyle() {
|
||||
if (editor) {
|
||||
editor.toggleStyle();
|
||||
} else {
|
||||
toggleSectionStyle();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSectionStyle() {
|
||||
$('#enabled').checked = !$('#enabled').checked;
|
||||
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) {
|
||||
if (
|
||||
cm.state.completionActive ||
|
||||
|
@ -1266,14 +1203,13 @@ function getEditorInSight(nearbyElement) {
|
|||
}
|
||||
|
||||
function beautify(event) {
|
||||
onDOMscripted([
|
||||
'vendor-overwrites/beautify/beautify-css-mod.js',
|
||||
() => {
|
||||
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
||||
.then(() => {
|
||||
if (!window.css_beautify && window.exports) {
|
||||
window.css_beautify = window.exports.css_beautify;
|
||||
}
|
||||
},
|
||||
]).then(doBeautify);
|
||||
})
|
||||
.then(doBeautify);
|
||||
|
||||
function doBeautify() {
|
||||
const tabs = prefs.get('editor.indentWithTabs');
|
||||
|
@ -1361,55 +1297,68 @@ onDOMready().then(init);
|
|||
|
||||
function init() {
|
||||
initCodeMirror();
|
||||
const params = getParams();
|
||||
if (!params.id) {
|
||||
getStyle().then(style => {
|
||||
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
|
||||
// This is an add
|
||||
$('#heading').textContent = t('addStyleTitle');
|
||||
const section = {code: ''};
|
||||
for (const i in CssToProperty) {
|
||||
if (params[i]) {
|
||||
section[CssToProperty[i]] = [params[i]];
|
||||
return Promise.resolve(createEmptyStyle());
|
||||
}
|
||||
}
|
||||
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');
|
||||
getStylesSafe({id: params.id}).then(styles => {
|
||||
// This is an edit
|
||||
return getStylesSafe({id}).then(styles => {
|
||||
let style = styles[0];
|
||||
if (!style) {
|
||||
style = {id: null, sections: []};
|
||||
style = createEmptyStyle();
|
||||
history.replaceState({}, document.title, location.pathname);
|
||||
}
|
||||
styleId = style.id;
|
||||
sessionStorage.justEditedStyleId = styleId;
|
||||
setStyleMeta(style);
|
||||
window.onload = () => {
|
||||
window.onload = null;
|
||||
initWithStyle({style});
|
||||
};
|
||||
if (document.readyState !== 'loading') {
|
||||
window.onload();
|
||||
}
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
function createEmptyStyle() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const style = {
|
||||
id: null,
|
||||
name: '',
|
||||
enabled: true,
|
||||
sections: [{code: ''}]
|
||||
};
|
||||
for (const i in CssToProperty) {
|
||||
if (params.get(i)) {
|
||||
style.sections[0][CssToProperty[i]] = [params.get(i)];
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
function setStyleMeta(style) {
|
||||
$('#name').value = style.name || '';
|
||||
$('#enabled').checked = style.enabled !== false;
|
||||
$('#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);
|
||||
|
||||
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() {
|
||||
if (initHooks.alreadyDone) {
|
||||
return;
|
||||
|
@ -1471,14 +1430,7 @@ function initHooks() {
|
|||
$('#keyMap-help').addEventListener('click', showKeyMapHelp, false);
|
||||
$('#cancel-button').addEventListener('click', goBackToManage);
|
||||
|
||||
$('#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;
|
||||
});
|
||||
|
||||
setupOptionsExpand();
|
||||
initLint();
|
||||
|
||||
if (!FIREFOX) {
|
||||
|
@ -1605,6 +1557,14 @@ function updateLintReportIfEnabled(...args) {
|
|||
}
|
||||
|
||||
function save() {
|
||||
if (editor) {
|
||||
editor.save();
|
||||
} else {
|
||||
saveSectionStyle();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSectionStyle() {
|
||||
updateLintReportIfEnabled(null, 0);
|
||||
|
||||
// save the contents of the CodeMirror editors back into the textareas
|
||||
|
@ -1679,17 +1639,7 @@ function showMozillaFormat() {
|
|||
}
|
||||
|
||||
function toMozillaFormat() {
|
||||
return getSectionsHashes().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');
|
||||
return mozParser.format({sections: getSectionsHashes()});
|
||||
}
|
||||
|
||||
function fromMozillaFormat() {
|
||||
|
@ -1714,133 +1664,29 @@ function fromMozillaFormat() {
|
|||
});
|
||||
|
||||
function doImport(event) {
|
||||
// parserlib contained in CSSLint-worker.js
|
||||
onDOMscripted(['vendor-overwrites/csslint/csslint-worker.js']).then(() => {
|
||||
doImportWhenReady(event.target);
|
||||
const replaceOldStyle = event.target.name === 'import-replace';
|
||||
const mozStyle = trimNewLines(popup.codebox.getValue());
|
||||
|
||||
mozParser.parse(mozStyle)
|
||||
.then(updateSection)
|
||||
.then(() => {
|
||||
editors.forEach(cm => updateLintReportIfEnabled(cm, 1));
|
||||
editors.last.state.renderLintReportNow = true;
|
||||
});
|
||||
}
|
||||
|
||||
function doImportWhenReady(target) {
|
||||
const replaceOldStyle = target.name === 'import-replace';
|
||||
$('.dismiss', popup).onclick();
|
||||
const mozStyle = trimNewLines(popup.codebox.getValue());
|
||||
const parser = new parserlib.css.Parser();
|
||||
const lines = mozStyle.split('\n');
|
||||
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
|
||||
const errors = [];
|
||||
// let oldSectionCount = editors.length;
|
||||
let firstAddedCM;
|
||||
})
|
||||
.catch(showError);
|
||||
|
||||
parser.addListener('startdocument', function (e) {
|
||||
let outerText = getRange(sectionStack.last.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));
|
||||
function showError(errors) {
|
||||
if (!Array.isArray(errors)) {
|
||||
errors = [errors];
|
||||
}
|
||||
if (outerText.trim()) {
|
||||
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({
|
||||
showHelp(t('styleFromMozillaFormatError'), $element({
|
||||
tag: 'pre',
|
||||
textContent: errors.join('\n'),
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
function updateSection(sections) {
|
||||
if (replaceOldStyle) {
|
||||
editors.slice(0).reverse().forEach(cm => {
|
||||
removeSection({target: cm.getSection().firstElementChild});
|
||||
|
@ -1851,17 +1697,24 @@ function fromMozillaFormat() {
|
|||
removeSection({target: editors.last.getSection()});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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']};
|
||||
}
|
||||
|
||||
const firstSection = sections[0];
|
||||
setCleanItem(addSection(null, firstSection), false);
|
||||
const firstAddedCM = editors.last;
|
||||
for (const section of sections.slice(1)) {
|
||||
setCleanItem(addSection(null, section), false);
|
||||
}
|
||||
|
||||
delete maximizeCodeHeight.stats;
|
||||
editors.forEach(cm => {
|
||||
maximizeCodeHeight(cm.getSection(), cm === editors.last);
|
||||
});
|
||||
|
||||
makeSectionVisible(firstAddedCM);
|
||||
firstAddedCM.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function trimNewLines(s) {
|
||||
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) {
|
||||
const div = $('#help-popup');
|
||||
div.classList.remove('big');
|
||||
|
@ -2182,40 +1890,55 @@ function showCodeMirrorPopup(title, html, options) {
|
|||
return popup;
|
||||
}
|
||||
|
||||
function getParams() {
|
||||
const params = {};
|
||||
const urlParts = location.href.split('?', 2);
|
||||
if (urlParts.length === 1) {
|
||||
return params;
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function replaceStyle(request) {
|
||||
const codeIsUpdated = request.codeIsUpdated !== false;
|
||||
if (!isUsercss(request.style)) {
|
||||
initWithSectionStyle(request);
|
||||
return;
|
||||
}
|
||||
urlParts[1].split('&').forEach(keyValue => {
|
||||
const splitKeyValue = keyValue.split('=', 2);
|
||||
params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]);
|
||||
});
|
||||
return params;
|
||||
if (!codeIsUpdated) {
|
||||
editor.replaceMeta(request.style);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
askDiscardChanges()
|
||||
.then(result => {
|
||||
if (result) {
|
||||
editor.replaceStyle(request.style);
|
||||
} else {
|
||||
editor.setStyleDirty(request.style);
|
||||
}
|
||||
});
|
||||
|
||||
function askDiscardChanges() {
|
||||
if (!editor.isTouched()) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return messageBox.confirm(t('styleUpdateDiscardChanges'));
|
||||
}
|
||||
}
|
||||
|
||||
function onRuntimeMessage(request) {
|
||||
switch (request.method) {
|
||||
case 'styleUpdated':
|
||||
if (styleId && styleId === request.style.id && request.reason !== 'editSave') {
|
||||
if (styleId && styleId === request.style.id && request.reason !== 'editSave' && request.reason !== 'config') {
|
||||
if ((request.style.sections[0] || {}).code === null) {
|
||||
// the code-less style came from notifyAllTabs
|
||||
onBackgroundReady().then(() => {
|
||||
request.style = BG.cachedStyles.byId.get(request.style.id);
|
||||
initWithStyle(request);
|
||||
replaceStyle(request);
|
||||
});
|
||||
} else {
|
||||
initWithStyle(request);
|
||||
replaceStyle(request);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
if (styleId && styleId === request.id) {
|
||||
if (styleId === request.id || editor && editor.getStyle().id === request.id) {
|
||||
window.onbeforeunload = () => {};
|
||||
window.close();
|
||||
closeCurrentTab();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -27,7 +27,7 @@ window.linterConfig.defaults.stylelint = (defaultSeverity => ({
|
|||
'property-no-unknown': [true, defaultSeverity],
|
||||
'selector-pseudo-class-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],
|
||||
'unit-no-unknown': [true, defaultSeverity],
|
||||
|
||||
|
|
123
edit/lint.js
123
edit/lint.js
|
@ -1,9 +1,10 @@
|
|||
/* global CodeMirror messageBox */
|
||||
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
|
||||
/* global onDOMscripted injectCSS require CSSLint stylelint */
|
||||
/* global loadScript require CSSLint stylelint */
|
||||
/* global makeLink */
|
||||
'use strict';
|
||||
|
||||
loadLinterAssets();
|
||||
onDOMready().then(loadLinterAssets);
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var linterConfig = {
|
||||
|
@ -20,18 +21,27 @@ var linterConfig = {
|
|||
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] || {});
|
||||
},
|
||||
|
||||
getForCodeMirror(linter = prefs.get('editor.linter')) {
|
||||
getForCodeMirror(linter = linterConfig.getDefault()) {
|
||||
return CodeMirror.lint && CodeMirror.lint[linter] ? {
|
||||
getAnnotations: CodeMirror.lint[linter],
|
||||
delay: prefs.get('editor.lintDelay'),
|
||||
} : false;
|
||||
},
|
||||
|
||||
fallbackToDefaults(config, linter = prefs.get('editor.linter')) {
|
||||
fallbackToDefaults(config, linter = linterConfig.getDefault()) {
|
||||
if (config && Object.keys(config).length) {
|
||||
if (linter === 'stylelint') {
|
||||
// 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 === 'csslint' || linter === 'stylelint' ? linter : '';
|
||||
if (prefs.get('editor.linter') !== linter) {
|
||||
if (linterConfig.getDefault() !== linter) {
|
||||
prefs.set('editor.linter', linter);
|
||||
}
|
||||
return linter;
|
||||
},
|
||||
|
||||
findInvalidRules(config, linter = prefs.get('editor.linter')) {
|
||||
findInvalidRules(config, linter = linterConfig.getDefault()) {
|
||||
const rules = linter === 'stylelint' ? config.rules : config;
|
||||
const allRules = new Set(
|
||||
linter === 'stylelint'
|
||||
|
@ -63,7 +73,7 @@ var linterConfig = {
|
|||
},
|
||||
|
||||
stringify(config = this.getCurrent()) {
|
||||
if (prefs.get('editor.linter') === 'stylelint') {
|
||||
if (linterConfig.getDefault() === 'stylelint') {
|
||||
config.syntax = undefined;
|
||||
}
|
||||
return JSON.stringify(config, null, 2)
|
||||
|
@ -72,7 +82,7 @@ var linterConfig = {
|
|||
|
||||
save(config) {
|
||||
config = this.fallbackToDefaults(config);
|
||||
const linter = prefs.get('editor.linter');
|
||||
const linter = linterConfig.getDefault();
|
||||
this[linter] = config;
|
||||
BG.chromeSync.setLZValue(this.storageName[linter], config);
|
||||
return config;
|
||||
|
@ -117,6 +127,13 @@ var linterConfig = {
|
|||
}
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
init() {
|
||||
if (!linterConfig.init.pending) {
|
||||
linterConfig.init.pending = linterConfig.loadAll();
|
||||
}
|
||||
return linterConfig.init.pending;
|
||||
}
|
||||
};
|
||||
|
||||
function initLint() {
|
||||
|
@ -130,21 +147,22 @@ function initLint() {
|
|||
$('#lint h2').addEventListener('click', toggleLintReport);
|
||||
}
|
||||
|
||||
linterConfig.loadAll();
|
||||
updateLinter();
|
||||
linterConfig.watchStorage();
|
||||
prefs.subscribe(['editor.linter'], updateLinter);
|
||||
updateLinter();
|
||||
}
|
||||
|
||||
function updateLinter({immediately} = {}) {
|
||||
function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
|
||||
if (!immediately) {
|
||||
debounce(updateLinter, 0, {immediately: true});
|
||||
debounce(updateLinter, 0, {immediately: true, linter});
|
||||
return;
|
||||
}
|
||||
const linter = prefs.get('editor.linter');
|
||||
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';
|
||||
$('#lint').style.display = 'none';
|
||||
|
||||
|
@ -357,13 +375,7 @@ function toggleLintReport() {
|
|||
}
|
||||
|
||||
function showLintHelp() {
|
||||
const makeLink = (href, textContent) => $element({
|
||||
tag: 'a',
|
||||
target: '_blank',
|
||||
href,
|
||||
textContent,
|
||||
});
|
||||
const linter = prefs.get('editor.linter');
|
||||
const linter = linterConfig.getDefault();
|
||||
const baseUrl = linter === 'stylelint'
|
||||
? 'https://stylelint.io/user-guide/rules/'
|
||||
// some CSSLint rules do not have a url
|
||||
|
@ -451,7 +463,7 @@ function setupLinterSettingsEvents(popup) {
|
|||
}
|
||||
|
||||
function setupLinterPopup(config) {
|
||||
const linter = prefs.get('editor.linter');
|
||||
const linter = linterConfig.getDefault();
|
||||
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
||||
|
||||
function makeButton(className, text, options = {}) {
|
||||
|
@ -503,43 +515,48 @@ function setupLinterPopup(config) {
|
|||
$('.save', popup).disabled = cm.isClean();
|
||||
});
|
||||
setupLinterSettingsEvents(popup);
|
||||
onDOMscripted([
|
||||
'vendor/codemirror/mode/javascript/javascript.js',
|
||||
'vendor/codemirror/addon/lint/json-lint.js',
|
||||
'vendor/jsonlint/jsonlint.js'
|
||||
loadScript([
|
||||
'/vendor/codemirror/mode/javascript/javascript.js',
|
||||
'/vendor/codemirror/addon/lint/json-lint.js',
|
||||
'/vendor/jsonlint/jsonlint.js'
|
||||
]).then(() => {
|
||||
popup.codebox.setOption('mode', 'application/json');
|
||||
popup.codebox.setOption('lint', 'json');
|
||||
});
|
||||
}
|
||||
|
||||
function loadLinterAssets(name = prefs.get('editor.linter')) {
|
||||
if (loadLinterAssets.loadingName === name) {
|
||||
return onDOMscripted();
|
||||
function loadLinterAssets(name = linterConfig.getDefault()) {
|
||||
if (!name) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
loadLinterAssets.loadingName = name;
|
||||
const scripts = [];
|
||||
return loadLibrary().then(loadAddon);
|
||||
|
||||
function loadLibrary() {
|
||||
if (name === 'csslint' && !window.CSSLint) {
|
||||
scripts.push(
|
||||
'vendor-overwrites/csslint/csslint-worker.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'
|
||||
);
|
||||
return loadScript([
|
||||
'/vendor-overwrites/csslint/csslint-worker.js',
|
||||
'/edit/lint-defaults-csslint.js'
|
||||
]);
|
||||
}
|
||||
if (name && !$('script[src$="vendor/codemirror/addon/lint/lint.js"]')) {
|
||||
injectCSS('vendor/codemirror/addon/lint/lint.css');
|
||||
injectCSS('msgbox/msgbox.css');
|
||||
scripts.push(
|
||||
'vendor/codemirror/addon/lint/lint.js',
|
||||
'edit/lint-codemirror-helper.js',
|
||||
'msgbox/msgbox.js'
|
||||
);
|
||||
if (name === 'stylelint' && !window.stylelint) {
|
||||
return loadScript([
|
||||
'/vendor-overwrites/stylelint/stylelint-bundle.min.js',
|
||||
'/edit/lint-defaults-stylelint.js'
|
||||
]).then(() => (window.stylelint = require('stylelint')));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function 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
|
||||
setTimeout(addTooltipsToEllipsized);
|
||||
// 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) {
|
||||
// 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) {
|
||||
// align to the top/bottom of the visible area if wasn't visible
|
||||
const bounds = element.getBoundingClientRect();
|
||||
|
@ -272,3 +192,19 @@ function $element(opt) {
|
|||
}
|
||||
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() {
|
||||
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
|
||||
const UIlang = chrome.i18n.getUILanguage();
|
||||
|
|
|
@ -382,15 +382,47 @@ function deleteStyleSafe({id, notify = true} = {}) {
|
|||
|
||||
function download(url) {
|
||||
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();
|
||||
xhr.timeout = 10e3;
|
||||
xhr.onloadend = () => (xhr.status === 200
|
||||
xhr.timeout = TIMEOUT;
|
||||
xhr.onload = () => (xhr.status === 200 || url.protocol === 'file:'
|
||||
? resolve(xhr.responseText)
|
||||
: reject(xhr.status));
|
||||
const [mainUrl, query] = url.split('?');
|
||||
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(query);
|
||||
xhr.onerror = reject;
|
||||
xhr.open(options.method, url.href, true);
|
||||
for (const key of Object.keys(options.headers)) {
|
||||
xhr.setRequestHeader(key, options.headers[key]);
|
||||
}
|
||||
xhr.send(options.body);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -400,3 +432,26 @@ function invokeOrPostpone(isInvoke, fn, ...args) {
|
|||
? 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
|
||||
'disableAll': false, // boss key
|
||||
'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.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.contextDelete': contextDeleteMissing(), // "Delete" item in context menu
|
||||
|
||||
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
|
||||
|
||||
'iconset': 0, // 0 = dark-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>
|
||||
<link rel="stylesheet" href="manage/manage.css">
|
||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
||||
<link rel="stylesheet" href="options/onoffswitch.css">
|
||||
|
||||
<style id="style-overrides"></style>
|
||||
|
||||
|
@ -83,6 +84,16 @@
|
|||
</svg>
|
||||
</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">
|
||||
<span class="updater-icons">
|
||||
<span class="check-update" i18n-title="checkForUpdate">
|
||||
|
@ -139,6 +150,8 @@
|
|||
<script src="manage/filters.js"></script>
|
||||
<script src="manage/updater-ui.js"></script>
|
||||
<script src="manage/object-diff.js"></script>
|
||||
<script src="js/color-parser.js"></script>
|
||||
<script src="manage/config-dialog.js"></script>
|
||||
<script src="manage/manage.js"></script>
|
||||
</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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/* global filtersSelector, filterAndAppend */
|
||||
/* global checkUpdate, handleUpdateInstalled */
|
||||
/* global objectDiff */
|
||||
/* global configDialog */
|
||||
'use strict';
|
||||
|
||||
let installed;
|
||||
|
@ -192,12 +193,19 @@ function createStyleElement({style, name}) {
|
|||
if (style.updateUrl && newUI.enabled) {
|
||||
$('.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()
|
||||
// which debounces its main loop thus loading the postponed favicons
|
||||
createStyleTargetsElement({entry, style, postponeFavicons: name});
|
||||
|
||||
return entry;
|
||||
|
||||
function shouldShowConfig() {
|
||||
return style.usercssData && Object.keys(style.usercssData.vars).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -275,6 +283,25 @@ Object.assign(handleEvent, {
|
|||
'.update': 'update',
|
||||
'.delete': 'delete',
|
||||
'.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) {
|
||||
|
@ -331,12 +358,18 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
update(event, entry) {
|
||||
// update everything but name
|
||||
saveStyleSafe(Object.assign(entry.updatedCode, {
|
||||
const request = Object.assign(entry.updatedCode, {
|
||||
id: entry.styleId,
|
||||
name: null,
|
||||
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) {
|
||||
|
|
|
@ -114,7 +114,11 @@ function reportUpdateState(state, style, details) {
|
|||
if (entry.classList.contains('can-update')) {
|
||||
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;
|
||||
entry.dataset.details = details;
|
||||
if (!details) {
|
||||
|
|
|
@ -22,9 +22,14 @@
|
|||
"scripts": [
|
||||
"js/messaging.js",
|
||||
"vendor-overwrites/lz-string/LZString-2xspeedup.js",
|
||||
"js/color-parser.js",
|
||||
"js/usercss.js",
|
||||
"background/storage.js",
|
||||
"background/usercss-helper.js",
|
||||
"js/prefs.js",
|
||||
"js/script-loader.js",
|
||||
"background/background.js",
|
||||
"vendor/node-semver/semver.js",
|
||||
"background/update.js"
|
||||
]
|
||||
},
|
||||
|
@ -49,6 +54,13 @@
|
|||
"run_at": "document_start",
|
||||
"all_frames": false,
|
||||
"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": {
|
||||
|
|
|
@ -40,17 +40,27 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#message-box.center #message-box-contents pre {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#message-box.center > div {
|
||||
top: unset;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#message-box.pre #message-box-contents {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
#message-box-title {
|
||||
font-weight: bold;
|
||||
background-color: rgb(145, 208, 198);
|
||||
padding: .75rem 24px .75rem 52px;
|
||||
font-size: 1rem;
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#message-box-title::before {
|
||||
|
|
|
@ -4,7 +4,7 @@ function messageBox({
|
|||
title, // [mandatory] string
|
||||
contents, // [mandatory] 1) DOM element 2) string
|
||||
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
|
||||
blockScroll, // boolean, blocks the page scroll
|
||||
}) { // RETURNS: Promise resolved to {button[number], enter[boolean], esc[boolean]}
|
||||
|
@ -69,14 +69,12 @@ function messageBox({
|
|||
onclick: messageBox.listeners.closeIcon}),
|
||||
$element({id: `${id}-contents`, appendChild: tHTML(contents)}),
|
||||
$element({id: `${id}-buttons`, appendChild:
|
||||
buttons.map((textContent, buttonIndex) => textContent &&
|
||||
$element({
|
||||
buttons.map((content, buttonIndex) => content && $element({
|
||||
tag: 'button',
|
||||
buttonIndex,
|
||||
textContent,
|
||||
onclick: messageBox.listeners.button,
|
||||
})
|
||||
)
|
||||
textContent: content.textContent || content,
|
||||
onclick: content.onclick || messageBox.listeners.button,
|
||||
}))
|
||||
}),
|
||||
]}),
|
||||
]});
|
||||
|
@ -101,3 +99,17 @@ function messageBox({
|
|||
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">
|
||||
<title i18n-text-append="optionsHeading">Stylus </title>
|
||||
<link rel="stylesheet" href="options/options.css">
|
||||
<link rel="stylesheet" href="options/onoffswitch.css">
|
||||
|
||||
<style id="firefox-transitions-bug-suppressor">
|
||||
/* restrict to FF */
|
||||
|
@ -122,6 +123,13 @@
|
|||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="optionsAdvancedNewStyleAsUsercss"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="newStyleAsUsercss">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
</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,
|
||||
input[type=number],
|
||||
input[type="color"],
|
||||
select,
|
||||
.onoffswitch {
|
||||
width: 60px;
|
||||
box-sizing: border-box;
|
||||
|
@ -221,63 +222,3 @@ sup {
|
|||
25% { opacity: 1 }
|
||||
100% { opacity: 0 }
|
||||
}
|
||||
|
||||
/* On/Off FlipSwitch https://proto.io/freebies/onoff/ */
|
||||
|
||||
.onoffswitch {
|
||||
position: relative;
|
||||
margin: 1ex 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.onoffswitch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onoffswitch span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
height: 12px;
|
||||
padding: 0;
|
||||
line-height: 12px;
|
||||
border: 0 solid #E3E3E3;
|
||||
border-radius: 12px;
|
||||
background-color: #E0E0E0;
|
||||
box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.onoffswitch span::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 18px;
|
||||
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