diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 48782997..8fbf088e 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -738,6 +738,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"
@@ -1087,50 +1275,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 7ab9dd6b..f5dbce9a 100644
--- a/background/background.js
+++ b/background/background.js
@@ -1,9 +1,13 @@
/* global detectSloppyRegexps download prefs openURL FIREFOX CHROME VIVALDI
openEditor debounce URLS ignoreChromeError queryTabs getTab
- usercss styleManager db msg navigatorUtil iconUtil
-*/
+ usercss styleManager db msg navigatorUtil iconUtil */
'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 || {}, {
getSectionsByUrl: styleManager.getSectionsByUrl,
getSectionsById: styleManager.getSectionsById,
@@ -26,7 +30,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,
diff --git a/background/parserlib-loader.js b/background/parserlib-loader.js
deleted file mode 100644
index cf5ec6e6..00000000
--- a/background/parserlib-loader.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* global importScripts parserlib 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/update.js b/background/update.js
index 0f089cf7..15256fc2 100644
--- a/background/update.js
+++ b/background/update.js
@@ -153,24 +153,27 @@
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 572f925c..e32c9f34 100644
--- a/background/usercss-helper.js
+++ b/background/usercss-helper.js
@@ -42,14 +42,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) {
@@ -62,7 +61,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;
@@ -95,7 +95,8 @@
function doBuild(style) {
if (vars) {
const oldStyle = {usercssData: {vars}};
- usercss.assignVars(style, oldStyle);
+ return usercss.assignVars(style, oldStyle)
+ .then(() => usercss.buildCode(style));
}
return usercss.buildCode(style);
}
diff --git a/edit.html b/edit.html
index b5b183c2..03eef893 100644
--- a/edit.html
+++ b/edit.html
@@ -70,6 +70,7 @@
+
@@ -81,8 +82,6 @@
-
-
diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js
index 4bf9498e..417c8bf2 100644
--- a/edit/codemirror-default.js
+++ b/edit/codemirror-default.js
@@ -351,8 +351,9 @@ CodeMirror.hint && (() => {
}
// USO vars in usercss mode editor
- const list = Object.keys(editor.getStyle().usercssData.vars)
- .filter(name => name.startsWith(leftPart));
+ const vars = editor.getStyle().usercssData.vars;
+ const list = vars ?
+ Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
return {
list,
from: {line, ch: prev},
diff --git a/edit/edit.js b/edit/edit.js
index d4dbd0b2..a0bad638 100644
--- a/edit/edit.js
+++ b/edit/edit.js
@@ -2,11 +2,14 @@
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
closeCurrentTab messageBox debounce
beautify
- moveFocus msg createSectionsEditor rerouteHotkeys
-*/
+ moveFocus msg createSectionsEditor rerouteHotkeys */
/* exported showCodeMirrorPopup */
'use strict';
+const editorWorker = workerUtil.createWorker({
+ url: '/edit/editor-worker.js'
+});
+
let saveSizeOnClose;
// direct & reverse mapping of @-moz-document keywords and internal property names
diff --git a/edit/editor-worker-body.js b/edit/editor-worker-body.js
deleted file mode 100644
index a4458dd6..00000000
--- a/edit/editor-worker-body.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/* global importScripts parseMozFormat parserlib CSSLint require */
-'use strict';
-
-createAPI({
- csslint: (code, config) => {
- loadParserLib();
- loadScript(['/vendor-overwrites/csslint/csslint.js']);
- return CSSLint.verify(code, config).messages
- .map(m => Object.assign(m, {rule: {id: m.rule.id}}));
- },
- stylelint: (code, config) => {
- loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
- return require('stylelint').lint({code, config});
- },
- parseMozFormat: data => {
- loadParserLib();
- loadScript(['/js/moz-parser.js']);
- return parseMozFormat(data);
- },
- getStylelintRules,
- getCsslintRules
-});
-
-function getCsslintRules() {
- loadScript(['/vendor-overwrites/csslint/csslint.js']);
- return CSSLint.getRules().map(rule => {
- const output = {};
- for (const [key, value] of Object.entries(rule)) {
- if (typeof value !== 'function') {
- output[key] = value;
- }
- }
- return output;
- });
-}
-
-function getStylelintRules() {
- loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
- const stylelint = require('stylelint');
- const options = {};
- const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
- const rxString = /"([-\w\s]{3,}?)"/g;
- for (const id of Object.keys(stylelint.rules)) {
- const ruleCode = String(stylelint.rules[id]);
- const sets = [];
- let m, mStr;
- while ((m = rxPossible.exec(ruleCode))) {
- const possible = m[1];
- const set = [];
- while ((mStr = rxString.exec(possible))) {
- const s = mStr[1];
- if (s.includes(' ')) {
- set.push(...s.split(/\s+/));
- } else {
- set.push(s);
- }
- }
- if (possible.includes('ignoreAtRules')) {
- set.push('ignoreAtRules');
- }
- if (possible.includes('ignoreShorthands')) {
- set.push('ignoreShorthands');
- }
- if (set.length) {
- sets.push(set);
- }
- }
- if (sets.length) {
- options[id] = sets;
- }
- }
- return options;
-}
-
-function loadParserLib() {
- if (typeof parserlib !== 'undefined') {
- return;
- }
- importScripts('/vendor-overwrites/csslint/parserlib.js');
- parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
-}
-
-const loadedUrls = new Set();
-function loadScript(urls) {
- urls = urls.filter(u => !loadedUrls.has(u));
- importScripts(...urls);
- urls.forEach(u => loadedUrls.add(u));
-}
-
-function createAPI(methods) {
- self.onmessage = e => {
- const message = e.data;
- Promise.resolve()
- .then(() => methods[message.action](...message.args))
- .then(result => ({
- id: message.id,
- error: false,
- data: result
- }))
- .catch(err => ({
- id: message.id,
- error: true,
- data: cloneError(err)
- }))
- .then(data => self.postMessage(data));
- };
-}
-
-function cloneError(err) {
- return Object.assign({
- name: err.name,
- stack: err.stack,
- message: err.message,
- lineNumber: err.lineNumber,
- columnNumber: err.columnNumber,
- fileName: err.fileName
- }, err);
-}
diff --git a/edit/editor-worker.js b/edit/editor-worker.js
index aaf4bd46..a84bb939 100644
--- a/edit/editor-worker.js
+++ b/edit/editor-worker.js
@@ -1,40 +1,89 @@
+/* global importScripts workerUtil CSSLint require metaParser */
/* exported editorWorker */
'use strict';
-// eslint-disable-next-line no-var
-var editorWorker = (() => {
- let worker;
- return new Proxy({}, {
- get: (target, prop) =>
- (...args) => {
- if (!worker) {
- worker = createWorker();
- }
- return worker.invoke(prop, args);
+importScripts('/js/worker-util.js');
+const {createAPI, loadScript} = workerUtil;
+
+createAPI({
+ csslint: (code, config) => {
+ loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
+ return CSSLint.verify(code, config).messages
+ .map(m => Object.assign(m, {rule: {id: m.rule.id}}));
+ },
+ stylelint: (code, config) => {
+ loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
+ return require('stylelint').lint({code, config});
+ },
+ metalint: code => {
+ loadScript(
+ '/vendor/usercss-meta/usercss-meta.min.js',
+ '/vendor-overwrites/colorpicker/colorconverter.js',
+ '/js/meta-parser.js'
+ );
+ const result = metaParser.lint(code);
+ // extract needed info
+ result.errors = result.errors.map(err =>
+ ({
+ code: err.code,
+ args: err.args,
+ message: err.message,
+ index: err.index
+ })
+ );
+ return result;
+ },
+ getStylelintRules,
+ getCsslintRules
+});
+
+function getCsslintRules() {
+ loadScript('/vendor-overwrites/csslint/csslint.js');
+ return CSSLint.getRules().map(rule => {
+ const output = {};
+ for (const [key, value] of Object.entries(rule)) {
+ if (typeof value !== 'function') {
+ output[key] = value;
}
+ }
+ return output;
});
+}
- function createWorker() {
- let id = 0;
- const pendingResponse = new Map();
- const worker = new Worker('/edit/editor-worker-body.js');
- worker.onmessage = e => {
- const message = e.data;
- pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
- pendingResponse.delete(message.id);
- };
- return {invoke};
-
- function invoke(action, args) {
- return new Promise((resolve, reject) => {
- pendingResponse.set(id, {resolve, reject});
- worker.postMessage({
- id,
- action,
- args
- });
- id++;
- });
+function getStylelintRules() {
+ loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
+ const stylelint = require('stylelint');
+ const options = {};
+ const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
+ const rxString = /"([-\w\s]{3,}?)"/g;
+ for (const id of Object.keys(stylelint.rules)) {
+ const ruleCode = String(stylelint.rules[id]);
+ const sets = [];
+ let m, mStr;
+ while ((m = rxPossible.exec(ruleCode))) {
+ const possible = m[1];
+ const set = [];
+ while ((mStr = rxString.exec(possible))) {
+ const s = mStr[1];
+ if (s.includes(' ')) {
+ set.push(...s.split(/\s+/));
+ } else {
+ set.push(s);
+ }
+ }
+ if (possible.includes('ignoreAtRules')) {
+ set.push('ignoreAtRules');
+ }
+ if (possible.includes('ignoreShorthands')) {
+ set.push('ignoreShorthands');
+ }
+ if (set.length) {
+ sets.push(set);
+ }
+ }
+ if (sets.length) {
+ options[id] = sets;
}
}
-})();
+ return options;
+}
diff --git a/edit/linter-meta.js b/edit/linter-meta.js
index b453df9c..99ab04f6 100644
--- a/edit/linter-meta.js
+++ b/edit/linter-meta.js
@@ -1,4 +1,4 @@
-/* global linter API */
+/* global linter API editorWorker */
/* exported createMetaCompiler */
'use strict';
@@ -19,25 +19,23 @@ function createMetaCompiler(cm) {
if (match[0] === meta && match.index === metaIndex) {
return cache;
}
- return API.parseUsercss({sourceCode: match[0], metaOnly: true})
- .then(result => result.usercssData)
- .then(result => {
- for (const cb of updateListeners) {
- cb(result);
+ return editorWorker.metalint(match[0])
+ .then(({metadata, errors}) => {
+ if (errors.every(err => err.code === 'unknownMeta')) {
+ for (const cb of updateListeners) {
+ cb(metadata);
+ }
}
+ cache = errors.map(err =>
+ ({
+ from: cm.posFromIndex((err.index || 0) + match.index),
+ to: cm.posFromIndex((err.index || 0) + match.index),
+ message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message,
+ severity: err.code === 'unknownMeta' ? 'warning' : 'error'
+ })
+ );
meta = match[0];
metaIndex = match.index;
- cache = [];
- return cache;
- }, err => {
- meta = match[0];
- metaIndex = match.index;
- cache = [{
- from: cm.posFromIndex((err.index || 0) + match.index),
- to: cm.posFromIndex((err.index || 0) + match.index),
- message: err.message,
- severity: 'error'
- }];
return cache;
});
});
diff --git a/edit/sections-editor.js b/edit/sections-editor.js
index 8d0e7ff7..1fdb39b3 100644
--- a/edit/sections-editor.js
+++ b/edit/sections-editor.js
@@ -432,7 +432,7 @@ function createSectionsEditor(style) {
function doImport({replaceOldStyle = false}) {
lockPageUI(true);
- editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
+ API.parseCss({code: popup.codebox.getValue().trim()})
.then(({sections, errors}) => {
// shouldn't happen but just in case
if (!sections.length || errors.length) {
diff --git a/edit/source-editor.js b/edit/source-editor.js
index c0476f80..ce2f4fa1 100644
--- a/edit/source-editor.js
+++ b/edit/source-editor.js
@@ -249,7 +249,7 @@ function createSourceEditor(style) {
.then(replaceStyle)
.catch(err => {
if (err.handled) return;
- if (err.message === t('styleMissingMeta', 'name')) {
+ if (err.code === 'missingMandatory' && err.args.includes('name')) {
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
chromeSync.setLZValue('usercssTemplate', code)
.then(() => chromeSync.getLZValue('usercssTemplate'))
@@ -258,7 +258,7 @@ function createSourceEditor(style) {
}
const contents = Array.isArray(err) ?
$create('pre', err.join('\n')) :
- [String(err)];
+ [err.message || String(err)];
if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index);
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js
index 8b375e60..3d2df039 100644
--- a/install-usercss/install-usercss.js
+++ b/install-usercss/install-usercss.js
@@ -241,7 +241,7 @@
const contents = Array.isArray(err) ?
[$create('pre', err.join('\n'))] :
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
- if (Number.isInteger(err.index)) {
+ if (Number.isInteger(err.index) && typeof contents[0] === 'string') {
const pos = cm.posFromIndex(err.index);
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
contents.push($create('pre', drawLinePointer(pos)));
diff --git a/js/meta-parser.js b/js/meta-parser.js
new file mode 100644
index 00000000..e660effb
--- /dev/null
+++ b/js/meta-parser.js
@@ -0,0 +1,78 @@
+/* global usercssMeta colorConverter */
+'use strict';
+
+// eslint-disable-next-line no-var
+var metaParser = (() => {
+ const {createParser, ParseError} = usercssMeta;
+ const PREPROCESSORS = new Set(['default', 'uso', 'stylus', 'less']);
+ const options = {
+ validateKey: {
+ preprocessor: state => {
+ if (!PREPROCESSORS.has(state.value)) {
+ throw new ParseError({
+ code: 'unknownPreprocessor',
+ args: [state.value],
+ index: state.valueIndex
+ });
+ }
+ }
+ },
+ validateVar: {
+ select: state => {
+ if (state.varResult.options.every(o => o.name !== state.value)) {
+ throw new ParseError({
+ code: 'invalidSelectValueMismatch',
+ index: state.valueIndex
+ });
+ }
+ },
+ color: state => {
+ const color = colorConverter.parse(state.value);
+ if (!color) {
+ throw new ParseError({
+ code: 'invalidColor',
+ args: [state.value],
+ index: state.valueIndex
+ });
+ }
+ state.value = colorConverter.format(color, 'rgb');
+ }
+ }
+ };
+ const parser = createParser(options);
+ const looseParser = createParser(Object.assign({}, options, {allowErrors: true, unknownKey: 'throw'}));
+ return {
+ parse,
+ lint,
+ nullifyInvalidVars
+ };
+
+ function parse(text, indexOffset) {
+ try {
+ return parser.parse(text);
+ } catch (err) {
+ if (typeof err.index === 'number') {
+ err.index += indexOffset;
+ }
+ throw err;
+ }
+ }
+
+ function lint(text) {
+ return looseParser.parse(text);
+ }
+
+ function nullifyInvalidVars(vars) {
+ for (const va of Object.values(vars)) {
+ if (va.value === null) {
+ continue;
+ }
+ try {
+ parser.validateVar(va);
+ } catch (err) {
+ va.value = null;
+ }
+ }
+ return vars;
+ }
+})();
diff --git a/js/sections-equal.js b/js/sections-util.js
similarity index 68%
rename from js/sections-equal.js
rename to js/sections-util.js
index 68260560..e85c4ea6 100644
--- a/js/sections-equal.js
+++ b/js/sections-util.js
@@ -1,6 +1,29 @@
/* exported styleSectionsEqual */
'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;
+
+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;
+}
+
/**
* @param {Style} a - first style object
* @param {Style} b - second style object
diff --git a/js/usercss.js b/js/usercss.js
index bb4efc5e..3d2bb5a8 100644
--- a/js/usercss.js
+++ b/js/usercss.js
@@ -1,514 +1,56 @@
-/* global loadScript semverCompare colorConverter styleCodeEmpty */
+/* global loadScript semverCompare colorConverter styleCodeEmpty backgroundWorker */
/* exported usercss */
'use strict';
const usercss = (() => {
- // true = global
- // false or 0 = private
- // = global key name
- // = (style, newValue)
- const KNOWN_META = new Map([
- ['author', true],
- ['advanced', 0],
- ['description', true],
- ['homepageURL', 'url'],
- ['icon', 0],
- ['license', 0],
- ['name', true],
- ['namespace', 0],
- //['noframes', 0],
- ['preprocessor', 0],
- ['supportURL', 0],
- ['updateURL', (style, newValue) => {
- // always preserve locally installed style's updateUrl
- if (!/^file:/.test(style.updateUrl)) {
- style.updateUrl = newValue;
- }
- }],
- ['var', 0],
- ['version', 0],
- ]);
- const MANDATORY_META = ['name', 'namespace', 'version'];
- const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image', 'number', 'range'];
- const META_URLS = [...KNOWN_META.keys()].filter(k => k.endsWith('URL'));
-
- const BUILDER = {
- default: {
- postprocess(sections, vars) {
- 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) {
- return loadScript('/vendor/stylus-lang-bundle/stylus.min.js').then(() => (
- new Promise((resolve, reject) => {
- const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
- if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
- window.stylus(varDef + source).render((err, output) => {
- if (err) {
- reject(err);
- } else {
- resolve(output);
- }
- });
- })
- ));
- }
- },
- less: {
- preprocess(source, vars) {
- window.less = window.less || {
- logLevel: 0,
- useFileCache: false,
- };
- const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
- return loadScript('/vendor/less/less.min.js')
- .then(() => window.less.render(varDefs + source))
- .then(({css}) => css);
- }
- },
- 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') {
- 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);
- });
- }
- }
- }
+ const GLOBAL_METAS = {
+ author: undefined,
+ description: undefined,
+ homepageURL: 'url',
+ // updateURL: 'updateUrl',
+ name: undefined,
};
-
- const RX_NUMBER = /-?\d+(\.\d+)?\s*/y;
- const RX_WHITESPACE = /\s*/y;
- const RX_WORD = /([\w-]+)\s*/y;
- const RX_STRING_BACKTICK = /(`(?:\\`|[\s\S])*?`)\s*/y;
- const RX_STRING_QUOTED = /((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/y;
-
- const worker = {};
-
- 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]
- };
- }
- }
- return {text: '', index: 0};
- }
-
- function parseWord(state, error = 'invalid word') {
- RX_WORD.lastIndex = state.re.lastIndex;
- const match = RX_WORD.exec(state.text);
- if (!match) {
- throw new Error((state.errorPrefix || '') + 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;
-
- const {re, type, text} = state;
-
- switch (type === 'image' && state.key === 'var' ? '@image@var' : type) {
- case 'checkbox': {
- const match = text.slice(re.lastIndex).match(/([01])\s+/);
- if (!match) {
- throw new Error('value must be 0 or 1');
- }
- re.lastIndex += match[0].length;
- result.default = match[1];
- break;
- }
-
- case 'select':
- case '@image@var': {
- state.errorPrefix = 'Invalid JSON: ';
- parseJSONValue(state);
- state.errorPrefix = '';
- const extractDefaultOption = (key, value) => {
- if (key.endsWith('*')) {
- const option = createOption(key.slice(0, -1), value);
- result.default = option.name;
- return option;
- }
- return createOption(key, value);
- };
- if (Array.isArray(state.value)) {
- result.options = state.value.map(k => extractDefaultOption(k));
- } else {
- result.options = Object.keys(state.value).map(k => extractDefaultOption(k, state.value[k]));
- }
- if (result.default === null) {
- result.default = (result.options[0] || {}).name || '';
- }
- break;
- }
-
- case 'number':
- case 'range': {
- state.errorPrefix = 'Invalid JSON: ';
- parseJSONValue(state);
- state.errorPrefix = '';
- // [default, start, end, step, units] (start, end, step & units are optional)
- if (Array.isArray(state.value) && state.value.length) {
- // label may be placed anywhere
- result.units = (state.value.find(i => typeof i === 'string') || '').replace(/[\d.+-]/g, '');
- const range = state.value.filter(i => typeof i === 'number' || i === null);
- result.default = range[0];
- result.min = range[1];
- result.max = range[2];
- result.step = range[3] === 0 ? 1 : range[3];
- }
- break;
- }
-
- case 'dropdown':
- case 'image': {
- if (text[re.lastIndex] !== '{') {
- throw new Error('no open {');
- }
- result.options = [];
- re.lastIndex++;
- while (text[re.lastIndex] !== '}') {
- const option = {};
-
- parseStringUnquoted(state);
- option.name = state.value;
-
- parseString(state);
- option.label = state.value;
-
- if (type === 'dropdown') {
- parseEOT(state);
- } else {
- parseString(state);
- }
- option.value = state.value;
-
- result.options.push(option);
- }
- re.lastIndex++;
- eatWhitespace(state);
- result.default = result.options[0].name;
- break;
- }
-
- default: {
- // text, color
- parseStringToEnd(state);
- result.default = state.value;
- }
- }
- state.usercssData.vars[result.name] = result;
- validateVar(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 = /<< {
- if (s[1] === q) {
- return q;
- }
- return JSON.parse(`"${s}"`);
- }
- );
- }
- return s;
- }
-
- function posOrEnd(haystack, needle, start) {
- const pos = haystack.indexOf(needle, start);
- return pos < 0 ? haystack.length : pos;
- }
+ const RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
+ const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
+ return {buildMeta, buildCode, assignVars};
function buildMeta(sourceCode) {
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
- const usercssData = {
- vars: {}
- };
-
const style = {
reason: 'install',
enabled: true,
sourceCode,
- sections: [],
- usercssData
+ sections: []
};
- const {text, index: metaIndex} = getMetaSource(sourceCode);
- const re = /@(\w+)[ \t\xA0]*/mg;
- const state = {style, re, text, usercssData};
+ const match = sourceCode.match(RX_META);
+ if (!match) {
+ throw new Error('can not find metadata');
+ }
- function doParse() {
- let match;
- while ((match = re.exec(text))) {
- const key = state.key = match[1];
- const route = KNOWN_META.get(key);
- if (route === undefined) {
- continue;
- }
- if (key === 'var' || key === 'advanced') {
- if (key === 'advanced') {
- state.maybeUSO = true;
+ return backgroundWorker.parseUsercssMeta(match[0], match.index)
+ .catch(err => {
+ if (err.code) {
+ const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
+ const message = chrome.i18n.getMessage(`meta_${err.code}`, args);
+ if (message) {
+ err.message = message;
}
- parseVar(state);
- } else {
- parseStringToEnd(state);
- usercssData[key] = state.value;
}
- let value = state.value;
- if (key === 'version') {
- value = usercssData[key] = normalizeVersion(value);
- validateVersion(value);
+ throw err;
+ })
+ .then(({metadata}) => {
+ style.usercssData = metadata;
+ for (const [key, value = key] of Object.entries(GLOBAL_METAS)) {
+ style[value] = metadata[key];
}
- if (META_URLS.includes(key)) {
- validateUrl(key, value);
- }
- switch (typeof route) {
- case 'function':
- route(style, value);
- break;
- case 'string':
- style[route] = value;
- break;
- default:
- if (route) {
- style[key] = value;
- }
- }
- }
- }
-
- try {
- doParse();
- } catch (e) {
- // the source code string offset
- e.index = metaIndex + state.re.lastIndex;
- throw e;
- }
-
- if (state.maybeUSO && !usercssData.preprocessor) {
- usercssData.preprocessor = 'uso';
- }
-
- validateStyle(style);
- return 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 drawList(items) {
+ return items.map(i => i.length === 1 ? JSON.stringify(i) : i).join(', ');
}
/**
@@ -518,136 +60,37 @@ const usercss = (() => {
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
*/
function buildCode(style, allowErrors) {
- 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(
- builder.preprocess && builder.preprocess(sourceCode, sVars) ||
- sourceCode)
- .then(mozStyle => invokeWorker({
- action: 'parse',
- styleId: style.id,
- code: mozStyle,
- }))
+ const match = style.sourceCode.match(RX_META);
+ return backgroundWorker.compileUsercss(
+ style.usercssData.preprocessor,
+ style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
+ style.usercssData.vars
+ )
.then(({sections, errors}) => {
if (!errors.length) errors = false;
if (!sections.length || errors && !allowErrors) {
- return Promise.reject(errors);
+ throw errors;
}
style.sections = sections;
- if (builder.postprocess) builder.postprocess(style.sections, sVars);
return allowErrors ? {style, errors} : 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;
- }
- if ((va.type === 'number' || va.type === 'range') && va.units) {
- return va[prop] + va.units;
- }
- return va[prop];
- }
-
- function validateStyle({usercssData: data}) {
- for (const prop of MANDATORY_META) {
- if (!data[prop]) {
- throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop));
- }
- }
- validateVersion(data.version);
- META_URLS.forEach(k => validateUrl(k, data[k]));
- Object.keys(data.vars).forEach(k => validateVar(data.vars[k]));
- }
-
- function validateVersion(version) {
- semverCompare(version, '0.0.0');
- }
-
- function validateUrl(key, url) {
- if (!url) {
- return;
- }
- url = new URL(url);
- if (!/^https?:/.test(url.protocol)) {
- throw new Error(`${url.protocol} is not a valid protocol in ${key}`);
- }
- }
-
- function validateVar(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] = colorConverter.format(colorConverter.parse(va[value]), 'rgb');
- } else if ((va.type === 'number' || va.type === 'range') && typeof va[value] !== 'number') {
- throw new Error(chrome.i18n.getMessage('styleMetaErrorRangeOrNumber', va.type));
- }
}
function assignVars(style, oldStyle) {
const {usercssData: {vars}} = style;
const {usercssData: {vars: oldVars}} = oldStyle;
+ if (!vars || !oldVars) {
+ return Promise.resolve();
+ }
// 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 {
- validateVar(vars[key], 'value');
- } catch (e) {
- vars[key].value = null;
- }
}
}
+ return backgroundWorker.nullifyInvalidVars(vars)
+ .then(vars => {
+ style.usercssData.vars = vars;
+ });
}
-
- function invokeWorker(message) {
- if (!worker.queue) {
- worker.instance = new Worker('/background/parserlib-loader.js');
- worker.queue = [];
- worker.instance.onmessage = ({data}) => {
- worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);
- if (worker.queue.length) {
- worker.instance.postMessage(worker.queue[0].message);
- }
- };
- }
- return new Promise(resolve => {
- worker.queue.push({message, resolve});
- if (worker.queue.length === 1) {
- worker.instance.postMessage(message);
- }
- });
- }
-
- return {buildMeta, buildCode, assignVars, invokeWorker};
})();
diff --git a/js/worker-util.js b/js/worker-util.js
new file mode 100644
index 00000000..5e081959
--- /dev/null
+++ b/js/worker-util.js
@@ -0,0 +1,98 @@
+/* global importScripts */
+'use strict';
+
+// eslint-disable-next-line no-var
+var workerUtil = (() => {
+ const loadedScripts = new Set();
+ return {createWorker, createAPI, loadScript, cloneError};
+
+ function createWorker({url, lifeTime = 30}) {
+ let worker;
+ let id;
+ let timer;
+ const pendingResponse = new Map();
+
+ return new Proxy({}, {
+ get: (target, prop) =>
+ (...args) => {
+ if (!worker) {
+ init();
+ }
+ return invoke(prop, args);
+ }
+ });
+
+ function init() {
+ id = 0;
+ worker = new Worker(url);
+ worker.onmessage = onMessage;
+ }
+
+ function uninit() {
+ worker.onmessage = null;
+ worker.terminate();
+ worker = null;
+ }
+
+ function onMessage(e) {
+ const message = e.data;
+ pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
+ pendingResponse.delete(message.id);
+ if (!pendingResponse.size && lifeTime >= 0) {
+ timer = setTimeout(uninit, lifeTime * 1000);
+ }
+ }
+
+ function invoke(action, args) {
+ return new Promise((resolve, reject) => {
+ pendingResponse.set(id, {resolve, reject});
+ clearTimeout(timer);
+ worker.postMessage({
+ id,
+ action,
+ args
+ });
+ id++;
+ });
+ }
+ }
+
+ function createAPI(methods) {
+ self.onmessage = e => {
+ const message = e.data;
+ Promise.resolve()
+ .then(() => methods[message.action](...message.args))
+ .then(result => ({
+ id: message.id,
+ error: false,
+ data: result
+ }))
+ .catch(err => ({
+ id: message.id,
+ error: true,
+ data: cloneError(err)
+ }))
+ .then(data => self.postMessage(data));
+ };
+ }
+
+ function cloneError(err) {
+ return Object.assign({
+ name: err.name,
+ stack: err.stack,
+ message: err.message,
+ lineNumber: err.lineNumber,
+ columnNumber: err.columnNumber,
+ fileName: err.fileName
+ }, err);
+ }
+
+ function loadScript(...scripts) {
+ const urls = scripts.filter(u => !loadedScripts.has(u));
+ if (!urls.length) {
+ return;
+ }
+ importScripts(...urls);
+ urls.forEach(u => loadedScripts.add(u));
+ }
+})();
diff --git a/manage.html b/manage.html
index 6c97fc4e..c68be215 100644
--- a/manage.html
+++ b/manage.html
@@ -172,7 +172,7 @@
-
+
diff --git a/manage/config-dialog.js b/manage/config-dialog.js
index a3188ee3..dc1b2477 100644
--- a/manage/config-dialog.js
+++ b/manage/config-dialog.js
@@ -184,7 +184,7 @@ function configDialog(style) {
.catch(errors => {
const el = $('.config-error', messageBox.element) ||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
- el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors;
+ el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors.message || String(errors);
})
.then(() => {
saving = false;
diff --git a/manifest.json b/manifest.json
index bf69609a..0550b064 100644
--- a/manifest.json
+++ b/manifest.json
@@ -27,7 +27,8 @@
"js/messaging.js",
"js/msg.js",
"js/storage-util.js",
- "js/sections-equal.js",
+ "js/sections-util.js",
+ "js/worker-util.js",
"background/storage.js",
"js/prefs.js",
"js/script-loader.js",
@@ -43,8 +44,7 @@
"background/search-db.js",
"background/update.js",
"background/openusercss-api.js",
- "vendor/semver-bundle/semver.js",
- "vendor-overwrites/colorpicker/colorconverter.js"
+ "vendor/semver-bundle/semver.js"
]
},
"commands": {
diff --git a/package.json b/package.json
index f0fdf8dc..4ae95fa9 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
"stylelint-bundle": "^8.0.0",
"stylus-lang-bundle": "^0.54.5",
"updates": "^4.2.1",
- "web-ext": "^2.9.1"
+ "web-ext": "^2.9.1",
+ "usercss-meta": "^0.8.1"
},
"scripts": {
"lint": "eslint **/*.js --cache || exit 0",
diff --git a/tools/update-libraries.js b/tools/update-libraries.js
index 13c4b126..8e490af1 100644
--- a/tools/update-libraries.js
+++ b/tools/update-libraries.js
@@ -28,6 +28,9 @@ const files = {
],
'stylus-lang-bundle': [
'stylus.min.js'
+ ],
+ 'usercss-meta': [
+ 'dist/usercss-meta.min.js → usercss-meta.min.js'
]
};
@@ -35,7 +38,7 @@ async function updateReadme(lib) {
const pkg = await fs.readJson(`${root}/node_modules/${lib}/package.json`);
const file = `${root}/vendor/${lib}/README.md`;
const txt = await fs.readFile(file, 'utf8');
- return fs.writeFile(file, txt.replace(/\bv[\d.]+[-\w]*\b/g, `v${pkg.version}`));
+ return fs.writeFile(file, txt.replace(/\b([v@])[\d.]+[-\w]*\b/g, `$1${pkg.version}`));
}
function isFolder(fileOrFolder) {
diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js
index 6e3a24e5..d5cbf67a 100644
--- a/vendor-overwrites/csslint/parserlib.js
+++ b/vendor-overwrites/csslint/parserlib.js
@@ -5505,3 +5505,5 @@ self.parserlib = (() => {
//endregion
})();
+
+self.parserlib.css.Tokens[self.parserlib.css.Tokens.COMMENT].hide = false;
diff --git a/vendor/README.md b/vendor/README.md
index fc0b6e78..9c519da7 100644
--- a/vendor/README.md
+++ b/vendor/README.md
@@ -9,7 +9,8 @@ Using this repo, run `npm install`... the latest versions of:
* `less` (https://github.com/less/less.js) is installed.
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
-* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.
+* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.
+* `usercss-meta` (https://github.com/StylishThemes/parse-usercss) is installed.
* The necessary build tools are installed; see `devDependencies` in the `package.json`.
## Running the build script
@@ -24,6 +25,7 @@ The following changes are made:
* `lz-string-unsafe`: The compressed `lz-string-unsafe.min.js` file is copied directly into `vendor/lz-string-unsafe`.
* `semver-bundle`: The `dist/semver.js` file is copied directly into `vendor/semver`.
* `stylus-lang-bundle`: The `stylus.min.js` file is copied directly into `vendor/stylus-lang-bundle`.
+* `usercss-meta`: The `dist/usercss-meta.min.js` file is copied directly into `vendor/usercss-meta`.
## Creating the ZIP
diff --git a/vendor/usercss-meta/LICENCE b/vendor/usercss-meta/LICENCE
new file mode 100644
index 00000000..4f7e567b
--- /dev/null
+++ b/vendor/usercss-meta/LICENCE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Original code: Copyright (c) Stylus team (github.com/openstyles/stylus)
+Modified code: Copyright (c) StylishThemes (github.com/StylishThemes/parse-usercss)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/usercss-meta/README.md b/vendor/usercss-meta/README.md
new file mode 100644
index 00000000..b1eb9236
--- /dev/null
+++ b/vendor/usercss-meta/README.md
@@ -0,0 +1,5 @@
+## usercss-meta v0.8.1
+
+usercss-meta installed via npm - source repo:
+
+https://unpkg.com/usercss-meta@0.8.1/dist/usercss-meta.min.js
diff --git a/vendor/usercss-meta/usercss-meta.min.js b/vendor/usercss-meta/usercss-meta.min.js
new file mode 100644
index 00000000..68c1c8fe
--- /dev/null
+++ b/vendor/usercss-meta/usercss-meta.min.js
@@ -0,0 +1,2 @@
+var usercssMeta=function(e){"use strict";class n extends Error{constructor(e){super(e.message),delete e.message,this.name="ParseError",Object.assign(this,e)}}class t extends n{constructor(e,n){super({code:"missingChar",args:e,message:`Missing character: ${e.map(e=>`'${e}'`).join(", ")}`,index:n})}}class a extends n{constructor(e){super({code:"EOF",message:"Unexpected end of file",index:e})}}const s=/<<e[1]===n?n:JSON.parse(`"${e}"`))}function v(e){l.lastIndex=e.lastIndex,l.exec(e.text),e.lastIndex=l.lastIndex}function y(e){i.lastIndex=e.lastIndex,e.lastIndex+=i.exec(e.text)[0].length}function g(e){if(e.lastIndex>=e.text.length)throw new a(e.lastIndex);e.index=e.lastIndex,e.value=e.text[e.lastIndex],e.lastIndex++,y(e)}function m(e){const t=e.lastIndex;o.lastIndex=t;const a=o.exec(e.text);if(!a)throw new n({code:"invalidWord",message:"Invalid word",index:t});e.index=t,e.value=a[1],e.lastIndex+=a[0].length}function h(e){const a=e.lastIndex;try{!function e(a){const{text:s}=a;if("{"===s[a.lastIndex]){const n={};for(a.lastIndex++,y(a);"}"!==s[a.lastIndex];){b(a);const l=a.value;if(":"!==s[a.lastIndex])throw new t([":"],a.lastIndex);if(a.lastIndex++,y(a),e(a),n[l]=a.value,","===s[a.lastIndex])a.lastIndex++,y(a);else if("}"!==s[a.lastIndex])throw new t([",","}"],a.lastIndex)}a.lastIndex++,y(a),a.value=n}else if("["===s[a.lastIndex]){const n=[];for(a.lastIndex++,y(a);"]"!==s[a.lastIndex];)if(e(a),n.push(a.value),","===s[a.lastIndex])a.lastIndex++,y(a);else if("]"!==s[a.lastIndex])throw new t([",","]"],a.lastIndex);a.lastIndex++,y(a),a.value=n}else if('"'===s[a.lastIndex]||"'"===s[a.lastIndex]||"`"===s[a.lastIndex])b(a);else if(/[-\d.]/.test(s[a.lastIndex]))O(a);else{if(m(a),!(a.value in x))throw new n({code:"unknownJSONLiteral",args:[a.value],message:`Unknown literal '${a.value}'`,index:a.index});a.value=x[a.value]}}(e)}catch(e){throw e.message=`Invalid JSON: ${e.message}`,e}e.index=a}function I(e){const t=e.lastIndex;s.lastIndex=t;const a=e.text.match(s);if(!a)throw new n({code:"missingEOT",message:"Missing EOT",index:t});e.index=t,e.lastIndex+=a[0].length,e.value=p(a[1].trim()),y(e)}function w(e){c.lastIndex=e.lastIndex;const n=c.exec(e.text);e.index=e.lastIndex,e.lastIndex=c.lastIndex,e.value=n[0].trim().replace(/\s+/g,"-")}function b(e){const t=e.lastIndex,a="`"===e.text[t]?u:d;a.lastIndex=t;const s=a.exec(e.text);if(!s)throw new n({code:"invalidString",message:"Invalid string",index:t});e.index=t,e.lastIndex+=s[0].length,e.value=f(s[1])}function O(e){const t=e.lastIndex;r.lastIndex=t;const a=r.exec(e.text);if(!a)throw new n({code:"invalidNumber",message:"Invalid number",index:t});e.index=t,e.value=Number(a[0].trim()),e.lastIndex+=a[0].length}function $(e){l.lastIndex=e.lastIndex;const n=l.exec(e.text);e.index=e.lastIndex,e.value=f(n[0].trim()),e.lastIndex=l.lastIndex}var k={eatLine:v,eatWhitespace:y,parseChar:g,parseEOT:I,parseJSON:h,parseNumber:O,parseString:b,parseStringToEnd:$,parseStringUnquoted:w,parseWord:m,unquote:f};const S=self.URL,R={name:$,version:$,namespace:$,author:$,description:$,homepageURL:$,supportURL:$,updateURL:$,license:$,preprocessor:$},E={version:function(e){if(!/\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi.test(e.value))throw new n({code:"invalidVersion",args:[e.value],message:`Invalid version: ${e.value}`,index:e.valueIndex});var t;e.value="v"===(t=e.value)[0]||"="===t[0]?t.slice(1):t},homepageURL:V,supportURL:V,updateURL:V},j={text:$,color:$,checkbox:g,select:J,dropdown:{advanced:D},image:{var:J,advanced:D},number:M,range:M},U={checkbox:function(e){if("1"!==e.value&&"0"!==e.value)throw new n({code:"invalidCheckboxDefault",message:"value must be 0 or 1",index:e.valueIndex})},number:_,range:_},N=["name","namespace","version"],T=["default","min","max","step"];function M(e){h(e);const t={min:null,max:null,step:null,units:null};if("number"==typeof e.value)t.default=e.value;else{if(!Array.isArray(e.value))throw new n({code:"invalidRange",message:"the default value must be an array or a number",index:e.valueIndex,args:[e.type]});{let a=0;for(const s of e.value)if("string"==typeof s){if(null!=t.units)throw new n({code:"invalidRangeMultipleUnits",message:"units is alredy defined",args:[e.type],index:e.valueIndex});t.units=s}else{if("number"!=typeof s&&null!==s)throw new n({code:"invalidRangeValue",message:"value must be number, string, or null",args:[e.type],index:e.valueIndex});if(a>=T.length)throw new n({code:"invalidRangeTooManyValues",message:"the array contains too many values",args:[e.type],index:e.valueIndex});t[T[a++]]=s}}}e.value=t.default,Object.assign(e.varResult,t)}function J(e){h(e);const t=Array.isArray(e.value)?e.value.map(e=>L(e)):Object.entries(e.value).map(([e,n])=>L(e,n));if(0===t.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:e.valueIndex});const a=t.filter(e=>e.isDefault);if(a.length>1)throw new n({code:"invalidSelectMultipleDefaults",message:"multiple default values",index:e.valueIndex});t.forEach(e=>{delete e.isDefault}),e.varResult.options=t,e.value=(a.length>0?a[0]:t[0]).name}function D(e){const a=e.lastIndex;if("{"!==e.text[e.lastIndex])throw new t(["{"],a);const s=[];for(e.lastIndex++;"}"!==e.text[e.lastIndex];){const n={};w(e),n.name=e.value,b(e),n.label=e.value,"dropdown"===e.type?I(e):b(e),n.value=e.value,s.push(n)}if(e.lastIndex++,y(e),0===s.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:a});"dropdown"===e.type&&(e.varResult.type="select",e.type="select"),e.varResult.options=s,e.value=s[0].name}function L(e,n){let t,a=!1;e.endsWith("*")&&(a=!0,e=e.slice(0,-1));const s=e.match(/^(\w+):(.*)/);return s&&([,t,e]=s),t||(t=e),n||(n=t),{name:t,label:e,value:n,isDefault:a}}function A(e,n){if(n)try{e()}catch(e){n.push(e)}else e()}function V(e){let t;try{t=new S(e.value)}catch(n){throw n.args=[e.value],n.index=e.valueIndex,n}if(!/^https?:/.test(t.protocol))throw new n({code:"invalidURLProtocol",args:[t.protocol],message:`Invalid protocol: ${t.protocol}`,index:e.valueIndex})}function _(e){const t=e.value;if("number"!=typeof t)throw new n({code:"invalidRangeDefault",message:`the default value of @var ${e.type} must be a number`,index:e.valueIndex,args:[e.type]});const a=e.varResult;if(null!=a.min&&ta.max)throw new n({code:"invalidRangeMax",message:"the value is larger than the maximum",index:e.valueIndex,args:[e.type]});if(null!=a.step){const s=a.step.toString().split(".")[1],l=s?10**s.length:0;if(t*l%(a.step*l))throw new n({code:"invalidRangeStep",message:"the value is not a multiple of the step",index:e.valueIndex,args:[e.type]})}}function K({unknownKey:e="ignore",mandatoryKeys:t=N,parseKey:a,parseVar:s,validateKey:l,validateVar:r,allowErrors:i=!1}={}){if(!["ignore","assign","throw"].includes(e))throw new TypeError("unknownKey must be 'ignore', 'assign', or 'throw'");const o=Object.assign({__proto__:null},R,a),u=Object.assign({},j,s),d=Object.assign({},E,l),c=Object.assign({},U,r);return{parse:function(e){if(e.includes("\r"))throw new TypeError("metadata includes invalid character: '\\r'");const a={},s=[],l=/@(\w+)[^\S\r\n]*/gm,r={index:0,lastIndex:0,text:e,usercssData:a,warn:e=>s.push(e)};let o;for(;o=l.exec(e);)r.index=o.index,r.lastIndex=l.lastIndex,r.key=o[1],r.shouldIgnore=!1,A(()=>{try{"var"===r.key||"advanced"===r.key?p(r):f(r)}catch(e){throw void 0===e.index&&(e.index=r.index),e}"var"===r.key||"advanced"===r.key||r.shouldIgnore||(a[r.key]=r.value)},i&&s),l.lastIndex=r.lastIndex;return r.maybeUSO&&!a.preprocessor&&(a.preprocessor="uso"),A(()=>{const e=t.filter(e=>!Object.prototype.hasOwnProperty.call(a,e));if(e.length>0)throw new n({code:"missingMandatory",args:e,message:`Missing metadata: ${e.map(e=>`@${e}`).join(", ")}`})},i&&s),{metadata:a,errors:s}},validateVar:function(e){x({key:"var",type:e.type,value:e.value,varResult:e})}};function x(e){const n="object"==typeof c[e.type]?c[e.type][e.key]:c[e.type];n&&n(e)}function p(e){const t={type:null,label:null,name:null,value:null,default:null,options:null};e.varResult=t,m(e),e.type=e.value,t.type=e.type;const a="object"==typeof u[e.type]?u[e.type][e.key]:u[e.type];if(!a)throw new n({code:"unknownVarType",message:`Unknown @${e.key} type: ${e.type}`,args:[e.key,e.type],index:e.index});m(e),t.name=e.value,b(e),t.label=e.value,e.valueIndex=e.lastIndex,a(e),x(e),t.default=e.value,e.usercssData.vars||(e.usercssData.vars={}),e.usercssData.vars[t.name]=t,"advanced"===e.key&&(e.maybeUSO=!0)}function f(t){let a=o[t.key];if(!a){if("assign"!==e){if(v(t),"ignore"===e)return void(t.shouldIgnore=!0);throw new n({code:"unknownMeta",args:[t.key],message:`Unknown metadata: @${t.key}`,index:t.index})}a=$}t.valueIndex=t.lastIndex,a(t),d[t.key]&&d[t.key](t)}}function P({alignKeys:e=!1,space:n=2,format:t="stylus",stringifyKey:a={},stringifyVar:s={}}={}){return{stringify:function(l){let r;if("stylus"===t)r="var";else{if("xstyle"!==t)throw new TypeError("options.format must be 'stylus' or 'xstyle'");r="advanced"}const i=[];for(const[e,o]of Object.entries(l))if(Object.prototype.hasOwnProperty.call(a,e)){const n=a[e](o);Array.isArray(n)?i.push(...n.map(n=>[e,n])):i.push([e,n])}else if("vars"===e)for(const e of Object.values(o))i.push([r,z(e,t,s,n)]);else if(Array.isArray(o))for(const n of o)i.push([e,W(n)]);else i.push([e,W(o)]);const o=e?Math.max(...i.map(e=>e[0].length)):0;return`/* ==UserStyle==\n${u=i.map(([e,n])=>`@${e.padEnd(o)} ${n}`).join("\n"),u.replace(/\*\//g,"*\\/")}\n==/UserStyle== */`;var u}}}function z(e,n,t,a){return`${"xstyle"===n&&"select"===e.type?"dropdown":e.type} ${e.name} ${JSON.stringify(e.label)} ${function(){if(Object.prototype.hasOwnProperty.call(t,e.type))return t[e.type](e,n,a);if(e.options)return"stylus"===n?JSON.stringify(e.options.reduce((n,t)=>{const a=t.name===e.default?"*":"";return n[`${t.name}:${t.label}${a}`]=t.value,n},{}),null,a):function(e,n=!1,t=0){const a="string"==typeof t?t:" ".repeat(t);return`{\n${e.map(e=>`${a}${e.name} ${JSON.stringify(e.label)} ${function(e){return n?JSON.stringify(e):`<<