diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1f2a3c1b..8fa5de10 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -689,6 +689,194 @@ "message": "Show active style count", "description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." }, + "meta_invalidCheckboxDefault": { + "message": "Invalid @var checkbox: value must be 0 or 1", + "description": "Error displayed when the value of @var checkbox is invalid" + }, + "meta_invalidColor": { + "message": "Invalid @var color: $color$ is not a color", + "description": "Error displayed when the value of @var color is invalid", + "placeholders": { + "color": { + "content": "$1" + } + } + }, + "meta_invalidRange": { + "message": "Invalid @var $type$: value must be a number or an array", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeMultipleUnits": { + "message": "Invalid @var $type$: multiple units are defined", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeTooManyValues": { + "message": "Invalid @var $type$: the array contains too many items", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeValue": { + "message": "Invalid @var $type$: items in the array must be number, string, or null", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeDefault": { + "message": "Invalid @var $type$: default value is null", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeMin": { + "message": "Invalid @var $type$: default value is lower than the minimum", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeMax": { + "message": "Invalid @var $type$: default value is larger than the maximum", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidRangeStep": { + "message": "Invalid @var $type$: default value is not a mutiple of the step", + "description": "Error displayed when the value of @var range or @var number is invalid", + "placeholders": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidSelectEmptyOptions": { + "message": "Invalid @var select: options list is empty", + "description": "Error displayed when the value of @var select is invalid" + }, + "meta_invalidSelectMultipleDefaults": { + "message": "Invalid @var select: multiple default options are defined", + "description": "Error displayed when the value of @var select is invalid" + }, + "meta_invalidSelectValueMismatch": { + "message": "Invalid @var select: value doesn't exist in the option list", + "description": "Error displayed when the value of @var select is invalid" + }, + "meta_invalidURLProtocol": { + "message": "Invalid URL protocol. Only http and https are allowed: $protocol$", + "description": "Error displayed when the protocol of the URL is invalid", + "placeholders": { + "protocol": { + "content": "$1" + } + } + }, + "meta_invalidVersion": { + "message": "Invalid version number. The value doesn't match SemVer pattern: $version$", + "description": "Error displayed when @version is invalid", + "placeholders": { + "version": { + "content": "$1" + } + } + }, + "meta_invalidNumber": { + "message": "Expect a number", + "description": "Error displayed when the value is expected to be a number" + }, + "meta_invalidString": { + "message": "Expect a quoted string", + "description": "Error displayed when the value is expected to be a quoted string" + }, + "meta_invalidWord": { + "message": "Expect a word", + "description": "Error displayed when the value is expected to be a word" + }, + "meta_missingChar": { + "message": "Expect characters: $chars$", + "description": "Error displayed when the value is expected to be some characters", + "placeholders": { + "chars": { + "content": "$1" + } + } + }, + "meta_missingEOT": { + "message": "Expect EOT data", + "description": "Error displayed when the value is expected to be an EOT list" + }, + "meta_missingMandatory": { + "message": "Missing mandatory metadata: $keys$", + "description": "Error displayed when mandatory keys are missing", + "placeholders": { + "keys": { + "content": "$1" + } + } + }, + "meta_unknownJSONLiteral": { + "message": "Invalid JSON: $literal$ is not a valid JSON literal", + "description": "Error displayed when JSON value is invalid", + "placeholders": { + "literal": { + "content": "$1" + } + } + }, + "meta_unknownMeta": { + "message": "Unknown metadata: $key$", + "description": "Error displayed when unknown metadata is parsed", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "meta_unknownVarType": { + "message": "Unknown @$varkey$ type: $vartype$", + "description": "Error displayed when unknown variable type is parsed", + "placeholders": { + "varkey": { + "content": "$1" + }, + "vartype": { + "content": "$2" + } + } + }, + "meta_unknownPreprocessor": { + "message": "Unknown @preprocessor: $preprocessor$", + "description": "Error displayed when unknown @preprocessor is parsed", + "placeholders": { + "preprocessor": { + "content": "$1" + } + } + }, "noStylesForSite": { "message": "No styles installed for this site.", "description": "Text displayed when no styles are installed for the current site" @@ -1038,50 +1226,6 @@ }, "description": "Confirmation when re-installing a style" }, - "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", - "placeholders": { - "color": { - "content": "$1" - } - }, - "description": "Error displayed when the value of @var color is invalid" - }, - "styleMetaErrorRangeOrNumber": { - "message": "Invalid @var $type$: value must be an array containing at least one number at index zero", - "description": "Error displayed when the value of @var number or @var range is invalid", - "placeholders": { - "type": { - "content": "$1" - } - } - }, - "styleMetaErrorPreprocessor": { - "message": "Unsupported @preprocessor: $preprocessor$", - "placeholders": { - "preprocessor": { - "content": "$1" - } - }, - "description": "Error displayed when the value of @preprocessor is not supported" - }, - "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$", - "placeholders": { - "key": { - "content": "$1" - } - }, - "description": "Error displayed when a mandatory metadata is missing" - }, "styleMissingName": { "message": "Enter a name", "description": "Error displayed when user saves without providing a name" diff --git a/background/background-worker.js b/background/background-worker.js new file mode 100644 index 00000000..630c33b0 --- /dev/null +++ b/background/background-worker.js @@ -0,0 +1,167 @@ +/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */ +'use strict'; + +importScripts('/js/worker-util.js'); +const {loadScript, createAPI} = workerUtil; + +createAPI({ + parseMozFormat(arg) { + loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); + return parseMozFormat(arg); + }, + compileUsercss, + parseUsercssMeta(text, indexOffset = 0) { + loadScript( + '/vendor/usercss-meta/usercss-meta.min.js', + '/vendor-overwrites/colorpicker/colorconverter.js', + '/js/meta-parser.js' + ); + return metaParser.parse(text, indexOffset); + }, + nullifyInvalidVars(vars) { + loadScript( + '/vendor/usercss-meta/usercss-meta.min.js', + '/vendor-overwrites/colorpicker/colorconverter.js', + '/js/meta-parser.js' + ); + return metaParser.nullifyInvalidVars(vars); + } +}); + +function compileUsercss(preprocessor, code, vars) { + loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); + const builder = getUsercssCompiler(preprocessor); + vars = simpleVars(vars); + return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code) + .then(code => parseMozFormat({code})) + .then(({sections, errors}) => { + if (builder.postprocess) { + builder.postprocess(sections, vars); + } + return {sections, errors}; + }); + + function simpleVars(vars) { + if (!vars) { + return {}; + } + // 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; + } + if ((va.type === 'number' || va.type === 'range') && va.units) { + return va[prop] + va.units; + } + return va[prop]; + } +} + +function getUsercssCompiler(preprocessor) { + const BUILDER = { + default: { + postprocess(sections, vars) { + loadScript('/js/sections-util.js'); + let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join(''); + if (!varDef) return; + varDef = ':root {\n' + varDef + '}\n'; + for (const section of sections) { + if (!styleCodeEmpty(section.code)) { + section.code = varDef + section.code; + } + } + } + }, + stylus: { + preprocess(source, vars) { + loadScript('/vendor/stylus-lang-bundle/stylus.min.js'); + return new Promise((resolve, reject) => { + const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join(''); + if (!Error.captureStackTrace) Error.captureStackTrace = () => {}; + self.stylus(varDef + source).render((err, output) => { + if (err) { + reject(err); + } else { + resolve(output); + } + }); + }); + } + }, + less: { + preprocess(source, vars) { + if (!self.less) { + self.less = { + logLevel: 0, + useFileCache: false, + }; + } + loadScript('/vendor/less/less.min.js'); + const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join(''); + return self.less.render(varDefs + source) + .then(({css}) => css); + } + }, + uso: { + preprocess(source, vars) { + loadScript('/vendor-overwrites/colorpicker/colorconverter.js'); + 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') { + const color = colorConverter.parse(vars[name].value); + if (!color) return null; + const {r, g, b} = color; + return `${r}, ${g}, ${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); + }); + } + } + } + }; + + if (preprocessor) { + if (!BUILDER[preprocessor]) { + throw new Error('unknwon preprocessor'); + } + return BUILDER[preprocessor]; + } + return BUILDER.default; +} diff --git a/background/background.js b/background/background.js index bbcd2954..ea6eb08a 100644 --- a/background/background.js +++ b/background/background.js @@ -4,10 +4,15 @@ global handleCssTransitionBug detectSloppyRegexps global openEditor global styleViaAPI global loadScript -global usercss +global usercss workerUtil */ 'use strict'; +// eslint-disable-next-line no-var +var backgroundWorker = workerUtil.createWorker({ + url: '/background/background-worker.js' +}); + window.API_METHODS = Object.assign(window.API_METHODS || {}, { getStyles, @@ -22,7 +27,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { return download(msg.url, msg); }, parseCss({code}) { - return usercss.invokeWorker({action: 'parse', code}); + return backgroundWorker.parseMozFormat({code}); }, getPrefs: prefs.getAll, healthCheck: () => dbExec().then(() => true), diff --git a/background/parserlib-loader.js b/background/parserlib-loader.js deleted file mode 100644 index 8275e8fd..00000000 --- a/background/parserlib-loader.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global importScripts parserlib CSSLint parseMozFormat */ -'use strict'; - -importScripts('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); -parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false; - -self.onmessage = ({data}) => { - self.postMessage(parseMozFormat(data)); -}; diff --git a/background/storage.js b/background/storage.js index b57cdf62..e373a8ed 100644 --- a/background/storage.js +++ b/background/storage.js @@ -1,9 +1,8 @@ -/* global getStyleWithNoCode styleSectionsEqual */ +/* + global getStyleWithNoCode styleSectionsEqual styleCodeEmpty RX_CSS_COMMENTS +*/ 'use strict'; -const RX_NAMESPACE = /\s*(@namespace\s+(?:\S+\s+)?url\(http:\/\/.*?\);)\s*/g; -const RX_CHARSET = /\s*@charset\s+(['"]).*?\1\s*;\s*/g; -const RX_CSS_COMMENTS = /\/\*[\s\S]*?(?:\*\/|$)/g; // eslint-disable-next-line no-var var SLOPPY_REGEXP_PREFIX = '\0'; @@ -527,26 +526,6 @@ function getApplicableSections({ } -function styleCodeEmpty(code) { - // Collect the global section if it's not empty, not comment-only, not namespace-only. - const cmtOpen = code && code.indexOf('/*'); - if (cmtOpen >= 0) { - const cmtCloseLast = code.lastIndexOf('*/'); - if (cmtCloseLast < 0) { - code = code.substr(0, cmtOpen); - } else { - code = code.substr(0, cmtOpen) + - code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') + - code.substr(cmtCloseLast + 2); - } - } - if (!code || !code.trim()) return true; - if (code.includes('@namespace')) code = code.replace(RX_NAMESPACE, '').trim(); - if (code.includes('@charset')) code = code.replace(RX_CHARSET, '').trim(); - return !code; -} - - function invalidateCache({added, updated, deletedId} = {}) { if (!cachedStyles.list) return; const id = added ? added.id : updated ? updated.id : deletedId; diff --git a/background/update.js b/background/update.js index 8426035e..b6c665b9 100644 --- a/background/update.js +++ b/background/update.js @@ -145,24 +145,27 @@ global API_METHODS function maybeUpdateUsercss() { // TODO: when sourceCode is > 100kB use http range request(s) for version check - 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) { - const sameCode = text === style.sourceCode; - return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); - } - break; - case 1: - // downgrade is always invalid - return Promise.reject(STATES.ERROR_VERSION); - } - return usercss.buildCode(json); - }); + return download(style.updateUrl) + .then(text => + usercss.buildMeta(text) + .then(json => { + 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) { + const sameCode = text === style.sourceCode; + return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); + } + break; + case 1: + // downgrade is always invalid + return Promise.reject(STATES.ERROR_VERSION); + } + return usercss.buildCode(json); + }) + ); } function maybeSave(json = {}) { diff --git a/background/usercss-helper.js b/background/usercss-helper.js index c2f2fe84..32525b28 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -40,14 +40,13 @@ 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); - } + + // allow sourceCode to be normalized + const {sourceCode} = style; + delete style.sourceCode; + + return usercss.buildMeta(sourceCode) + .then(newStyle => Object.assign(newStyle, style)); } function assignVars(style) { @@ -59,7 +58,8 @@ style.id = dup.id; if (style.reason !== 'config') { // preserve style.vars during update - usercss.assignVars(style, dup); + return usercss.assignVars(style, dup) + .then(() => style); } } return style; diff --git a/edit.html b/edit.html index 8463a984..8787c2dc 100644 --- a/edit.html +++ b/edit.html @@ -24,6 +24,7 @@ + @@ -96,8 +97,6 @@ - -