Merge branch 'dev-usercss-meta' into dev-exclusions
This commit is contained in:
commit
bd4a453f45
|
@ -738,6 +738,194 @@
|
||||||
"message": "Show active style count",
|
"message": "Show active style count",
|
||||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
"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": {
|
"noStylesForSite": {
|
||||||
"message": "No styles installed for this site.",
|
"message": "No styles installed for this site.",
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
|
@ -1087,50 +1275,6 @@
|
||||||
},
|
},
|
||||||
"description": "Confirmation when re-installing a style"
|
"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": {
|
"styleMissingName": {
|
||||||
"message": "Enter a name",
|
"message": "Enter a name",
|
||||||
"description": "Error displayed when user saves without providing a name"
|
"description": "Error displayed when user saves without providing a name"
|
||||||
|
|
167
background/background-worker.js
Normal file
167
background/background-worker.js
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
/* global detectSloppyRegexps download prefs openURL FIREFOX CHROME VIVALDI
|
/* global detectSloppyRegexps download prefs openURL FIREFOX CHROME VIVALDI
|
||||||
openEditor debounce URLS ignoreChromeError queryTabs getTab
|
openEditor debounce URLS ignoreChromeError queryTabs getTab
|
||||||
usercss styleManager db msg navigatorUtil iconUtil
|
usercss styleManager db msg navigatorUtil iconUtil */
|
||||||
*/
|
|
||||||
'use strict';
|
'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 || {}, {
|
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||||
getSectionsByUrl: styleManager.getSectionsByUrl,
|
getSectionsByUrl: styleManager.getSectionsByUrl,
|
||||||
getSectionsById: styleManager.getSectionsById,
|
getSectionsById: styleManager.getSectionsById,
|
||||||
|
@ -26,7 +30,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||||
return download(msg.url, msg);
|
return download(msg.url, msg);
|
||||||
},
|
},
|
||||||
parseCss({code}) {
|
parseCss({code}) {
|
||||||
return usercss.invokeWorker({action: 'parse', code});
|
return backgroundWorker.parseMozFormat({code});
|
||||||
},
|
},
|
||||||
getPrefs: prefs.getAll,
|
getPrefs: prefs.getAll,
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
||||||
};
|
|
|
@ -153,8 +153,10 @@
|
||||||
|
|
||||||
function maybeUpdateUsercss() {
|
function maybeUpdateUsercss() {
|
||||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||||
return download(style.updateUrl).then(text => {
|
return download(style.updateUrl)
|
||||||
const json = usercss.buildMeta(text);
|
.then(text =>
|
||||||
|
usercss.buildMeta(text)
|
||||||
|
.then(json => {
|
||||||
const {usercssData: {version}} = style;
|
const {usercssData: {version}} = style;
|
||||||
const {usercssData: {version: newVersion}} = json;
|
const {usercssData: {version: newVersion}} = json;
|
||||||
switch (Math.sign(semverCompare(version, newVersion))) {
|
switch (Math.sign(semverCompare(version, newVersion))) {
|
||||||
|
@ -170,7 +172,8 @@
|
||||||
return Promise.reject(STATES.ERROR_VERSION);
|
return Promise.reject(STATES.ERROR_VERSION);
|
||||||
}
|
}
|
||||||
return usercss.buildCode(json);
|
return usercss.buildCode(json);
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeSave(json = {}) {
|
function maybeSave(json = {}) {
|
||||||
|
|
|
@ -42,14 +42,13 @@
|
||||||
if (style.usercssData) {
|
if (style.usercssData) {
|
||||||
return Promise.resolve(style);
|
return Promise.resolve(style);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const {sourceCode} = style;
|
|
||||||
// allow sourceCode to be normalized
|
// allow sourceCode to be normalized
|
||||||
|
const {sourceCode} = style;
|
||||||
delete style.sourceCode;
|
delete style.sourceCode;
|
||||||
return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style));
|
|
||||||
} catch (e) {
|
return usercss.buildMeta(sourceCode)
|
||||||
return Promise.reject(e);
|
.then(newStyle => Object.assign(newStyle, style));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignVars(style) {
|
function assignVars(style) {
|
||||||
|
@ -62,7 +61,8 @@
|
||||||
style.id = dup.id;
|
style.id = dup.id;
|
||||||
if (style.reason !== 'config') {
|
if (style.reason !== 'config') {
|
||||||
// preserve style.vars during update
|
// preserve style.vars during update
|
||||||
usercss.assignVars(style, dup);
|
return usercss.assignVars(style, dup)
|
||||||
|
.then(() => style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return style;
|
return style;
|
||||||
|
@ -95,7 +95,8 @@
|
||||||
function doBuild(style) {
|
function doBuild(style) {
|
||||||
if (vars) {
|
if (vars) {
|
||||||
const oldStyle = {usercssData: {vars}};
|
const oldStyle = {usercssData: {vars}};
|
||||||
usercss.assignVars(style, oldStyle);
|
return usercss.assignVars(style, oldStyle)
|
||||||
|
.then(() => usercss.buildCode(style));
|
||||||
}
|
}
|
||||||
return usercss.buildCode(style);
|
return usercss.buildCode(style);
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
<script src="js/script-loader.js"></script>
|
<script src="js/script-loader.js"></script>
|
||||||
<script src="js/storage-util.js"></script>
|
<script src="js/storage-util.js"></script>
|
||||||
<script src="js/msg.js"></script>
|
<script src="js/msg.js"></script>
|
||||||
|
<script src="js/worker-util.js"></script>
|
||||||
|
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
|
|
||||||
|
@ -81,8 +82,6 @@
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
|
|
||||||
<script src="edit/editor-worker.js"></script>
|
|
||||||
|
|
||||||
<script src="edit/util.js"></script>
|
<script src="edit/util.js"></script>
|
||||||
<script src="edit/regexp-tester.js"></script>
|
<script src="edit/regexp-tester.js"></script>
|
||||||
<script src="edit/live-preview.js"></script>
|
<script src="edit/live-preview.js"></script>
|
||||||
|
|
|
@ -351,8 +351,9 @@ CodeMirror.hint && (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// USO vars in usercss mode editor
|
// USO vars in usercss mode editor
|
||||||
const list = Object.keys(editor.getStyle().usercssData.vars)
|
const vars = editor.getStyle().usercssData.vars;
|
||||||
.filter(name => name.startsWith(leftPart));
|
const list = vars ?
|
||||||
|
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
from: {line, ch: prev},
|
from: {line, ch: prev},
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
|
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
|
||||||
closeCurrentTab messageBox debounce
|
closeCurrentTab messageBox debounce
|
||||||
beautify
|
beautify
|
||||||
moveFocus msg createSectionsEditor rerouteHotkeys
|
moveFocus msg createSectionsEditor rerouteHotkeys */
|
||||||
*/
|
|
||||||
/* exported showCodeMirrorPopup */
|
/* exported showCodeMirrorPopup */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const editorWorker = workerUtil.createWorker({
|
||||||
|
url: '/edit/editor-worker.js'
|
||||||
|
});
|
||||||
|
|
||||||
let saveSizeOnClose;
|
let saveSizeOnClose;
|
||||||
|
|
||||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,40 +1,89 @@
|
||||||
|
/* global importScripts workerUtil CSSLint require metaParser */
|
||||||
/* exported editorWorker */
|
/* exported editorWorker */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
importScripts('/js/worker-util.js');
|
||||||
var editorWorker = (() => {
|
const {createAPI, loadScript} = workerUtil;
|
||||||
let worker;
|
|
||||||
return new Proxy({}, {
|
|
||||||
get: (target, prop) =>
|
|
||||||
(...args) => {
|
|
||||||
if (!worker) {
|
|
||||||
worker = createWorker();
|
|
||||||
}
|
|
||||||
return worker.invoke(prop, args);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function createWorker() {
|
createAPI({
|
||||||
let id = 0;
|
csslint: (code, config) => {
|
||||||
const pendingResponse = new Map();
|
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
|
||||||
const worker = new Worker('/edit/editor-worker-body.js');
|
return CSSLint.verify(code, config).messages
|
||||||
worker.onmessage = e => {
|
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||||
const message = e.data;
|
},
|
||||||
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
|
stylelint: (code, config) => {
|
||||||
pendingResponse.delete(message.id);
|
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||||
};
|
return require('stylelint').lint({code, config});
|
||||||
return {invoke};
|
},
|
||||||
|
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 invoke(action, args) {
|
function getCsslintRules() {
|
||||||
return new Promise((resolve, reject) => {
|
loadScript('/vendor-overwrites/csslint/csslint.js');
|
||||||
pendingResponse.set(id, {resolve, reject});
|
return CSSLint.getRules().map(rule => {
|
||||||
worker.postMessage({
|
const output = {};
|
||||||
id,
|
for (const [key, value] of Object.entries(rule)) {
|
||||||
action,
|
if (typeof value !== 'function') {
|
||||||
args
|
output[key] = value;
|
||||||
});
|
|
||||||
id++;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global linter API */
|
/* global linter API editorWorker */
|
||||||
/* exported createMetaCompiler */
|
/* exported createMetaCompiler */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -19,25 +19,23 @@ function createMetaCompiler(cm) {
|
||||||
if (match[0] === meta && match.index === metaIndex) {
|
if (match[0] === meta && match.index === metaIndex) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
return API.parseUsercss({sourceCode: match[0], metaOnly: true})
|
return editorWorker.metalint(match[0])
|
||||||
.then(result => result.usercssData)
|
.then(({metadata, errors}) => {
|
||||||
.then(result => {
|
if (errors.every(err => err.code === 'unknownMeta')) {
|
||||||
for (const cb of updateListeners) {
|
for (const cb of updateListeners) {
|
||||||
cb(result);
|
cb(metadata);
|
||||||
}
|
}
|
||||||
meta = match[0];
|
}
|
||||||
metaIndex = match.index;
|
cache = errors.map(err =>
|
||||||
cache = [];
|
({
|
||||||
return cache;
|
|
||||||
}, err => {
|
|
||||||
meta = match[0];
|
|
||||||
metaIndex = match.index;
|
|
||||||
cache = [{
|
|
||||||
from: cm.posFromIndex((err.index || 0) + match.index),
|
from: cm.posFromIndex((err.index || 0) + match.index),
|
||||||
to: cm.posFromIndex((err.index || 0) + match.index),
|
to: cm.posFromIndex((err.index || 0) + match.index),
|
||||||
message: err.message,
|
message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message,
|
||||||
severity: 'error'
|
severity: err.code === 'unknownMeta' ? 'warning' : 'error'
|
||||||
}];
|
})
|
||||||
|
);
|
||||||
|
meta = match[0];
|
||||||
|
metaIndex = match.index;
|
||||||
return cache;
|
return cache;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -432,7 +432,7 @@ function createSectionsEditor(style) {
|
||||||
|
|
||||||
function doImport({replaceOldStyle = false}) {
|
function doImport({replaceOldStyle = false}) {
|
||||||
lockPageUI(true);
|
lockPageUI(true);
|
||||||
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
|
API.parseCss({code: popup.codebox.getValue().trim()})
|
||||||
.then(({sections, errors}) => {
|
.then(({sections, errors}) => {
|
||||||
// shouldn't happen but just in case
|
// shouldn't happen but just in case
|
||||||
if (!sections.length || errors.length) {
|
if (!sections.length || errors.length) {
|
||||||
|
|
|
@ -249,7 +249,7 @@ function createSourceEditor(style) {
|
||||||
.then(replaceStyle)
|
.then(replaceStyle)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.handled) return;
|
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 &&
|
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
||||||
chromeSync.setLZValue('usercssTemplate', code)
|
chromeSync.setLZValue('usercssTemplate', code)
|
||||||
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
||||||
|
@ -258,7 +258,7 @@ function createSourceEditor(style) {
|
||||||
}
|
}
|
||||||
const contents = Array.isArray(err) ?
|
const contents = Array.isArray(err) ?
|
||||||
$create('pre', err.join('\n')) :
|
$create('pre', err.join('\n')) :
|
||||||
[String(err)];
|
[err.message || String(err)];
|
||||||
if (Number.isInteger(err.index)) {
|
if (Number.isInteger(err.index)) {
|
||||||
const pos = cm.posFromIndex(err.index);
|
const pos = cm.posFromIndex(err.index);
|
||||||
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
|
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
|
||||||
|
|
|
@ -241,7 +241,7 @@
|
||||||
const contents = Array.isArray(err) ?
|
const contents = Array.isArray(err) ?
|
||||||
[$create('pre', err.join('\n'))] :
|
[$create('pre', err.join('\n'))] :
|
||||||
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
|
[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);
|
const pos = cm.posFromIndex(err.index);
|
||||||
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
|
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
|
||||||
contents.push($create('pre', drawLinePointer(pos)));
|
contents.push($create('pre', drawLinePointer(pos)));
|
||||||
|
|
78
js/meta-parser.js
Normal file
78
js/meta-parser.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,6 +1,29 @@
|
||||||
/* exported styleSectionsEqual */
|
/* exported styleSectionsEqual */
|
||||||
'use strict';
|
'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} a - first style object
|
||||||
* @param {Style} b - second style object
|
* @param {Style} b - second style object
|
645
js/usercss.js
645
js/usercss.js
|
@ -1,514 +1,56 @@
|
||||||
/* global loadScript semverCompare colorConverter styleCodeEmpty */
|
/* global loadScript semverCompare colorConverter styleCodeEmpty backgroundWorker */
|
||||||
/* exported usercss */
|
/* exported usercss */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const usercss = (() => {
|
const usercss = (() => {
|
||||||
// true = global
|
const GLOBAL_METAS = {
|
||||||
// false or 0 = private
|
author: undefined,
|
||||||
// <string> = global key name
|
description: undefined,
|
||||||
// <function> = (style, newValue)
|
homepageURL: 'url',
|
||||||
const KNOWN_META = new Map([
|
// updateURL: 'updateUrl',
|
||||||
['author', true],
|
name: undefined,
|
||||||
['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('');
|
const RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
|
||||||
return loadScript('/vendor/less/less.min.js')
|
const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
|
||||||
.then(() => window.less.render(varDefs + source))
|
return {buildMeta, buildCode, assignVars};
|
||||||
.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 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 = /<<<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 pos = state.re.lastIndex;
|
|
||||||
const nextQuoteOrEOL = posOrEnd(state.text, '"', pos);
|
|
||||||
state.re.lastIndex = nextQuoteOrEOL;
|
|
||||||
state.value = state.text.slice(pos, nextQuoteOrEOL).trim().replace(/\s+/g, '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseString(state) {
|
|
||||||
const pos = state.re.lastIndex;
|
|
||||||
const rx = state.text[pos] === '`' ? RX_STRING_BACKTICK : RX_STRING_QUOTED;
|
|
||||||
rx.lastIndex = pos;
|
|
||||||
const match = rx.exec(state.text);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error((state.errorPrefix || '') + 'Quoted string expected');
|
|
||||||
}
|
|
||||||
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
|
|
||||||
};
|
|
||||||
const {text, re, errorPrefix} = state;
|
|
||||||
if (text[re.lastIndex] === '{') {
|
|
||||||
// object
|
|
||||||
const obj = {};
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
while (text[re.lastIndex] !== '}') {
|
|
||||||
parseString(state);
|
|
||||||
const key = state.value;
|
|
||||||
if (text[re.lastIndex] !== ':') {
|
|
||||||
throw new Error(`${errorPrefix}missing ':'`);
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
parseJSONValue(state);
|
|
||||||
obj[key] = state.value;
|
|
||||||
if (text[re.lastIndex] === ',') {
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
} else if (text[re.lastIndex] !== '}') {
|
|
||||||
throw new Error(`${errorPrefix}missing ',' or '}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
state.value = obj;
|
|
||||||
} else if (text[re.lastIndex] === '[') {
|
|
||||||
// array
|
|
||||||
const arr = [];
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
while (text[re.lastIndex] !== ']') {
|
|
||||||
parseJSONValue(state);
|
|
||||||
arr.push(state.value);
|
|
||||||
if (text[re.lastIndex] === ',') {
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
} else if (text[re.lastIndex] !== ']') {
|
|
||||||
throw new Error(`${errorPrefix}missing ',' or ']'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
state.value = arr;
|
|
||||||
} else if (text[re.lastIndex] === '"' || text[re.lastIndex] === '`') {
|
|
||||||
// string
|
|
||||||
parseString(state);
|
|
||||||
} else if (/\d/.test(text[re.lastIndex])) {
|
|
||||||
// number
|
|
||||||
parseNumber(state);
|
|
||||||
} else {
|
|
||||||
parseWord(state);
|
|
||||||
if (!(state.value in JSON_PRIME)) {
|
|
||||||
throw new Error(`${errorPrefix}unknown literal '${state.value}'`);
|
|
||||||
}
|
|
||||||
state.value = JSON_PRIME[state.value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseNumber(state) {
|
|
||||||
RX_NUMBER.lastIndex = state.re.lastIndex;
|
|
||||||
const match = RX_NUMBER.exec(state.text);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error((state.errorPrefix || '') + 'invalid number');
|
|
||||||
}
|
|
||||||
state.value = Number(match[0].trim());
|
|
||||||
state.re.lastIndex += match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eatWhitespace(state) {
|
|
||||||
RX_WHITESPACE.lastIndex = state.re.lastIndex;
|
|
||||||
state.re.lastIndex += RX_WHITESPACE.exec(state.text)[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStringToEnd(state) {
|
|
||||||
const EOL = posOrEnd(state.text, '\n', state.re.lastIndex);
|
|
||||||
const match = state.text.slice(state.re.lastIndex, EOL);
|
|
||||||
state.value = unquote(match.trim());
|
|
||||||
state.re.lastIndex += match.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unquote(s) {
|
|
||||||
const q = s[0];
|
|
||||||
if (q === s[s.length - 1] && (q === '"' || q === "'" || 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 posOrEnd(haystack, needle, start) {
|
|
||||||
const pos = haystack.indexOf(needle, start);
|
|
||||||
return pos < 0 ? haystack.length : pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMeta(sourceCode) {
|
function buildMeta(sourceCode) {
|
||||||
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
||||||
|
|
||||||
const usercssData = {
|
|
||||||
vars: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
reason: 'install',
|
reason: 'install',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sourceCode,
|
sourceCode,
|
||||||
sections: [],
|
sections: []
|
||||||
usercssData
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const {text, index: metaIndex} = getMetaSource(sourceCode);
|
const match = sourceCode.match(RX_META);
|
||||||
const re = /@(\w+)[ \t\xA0]*/mg;
|
if (!match) {
|
||||||
const state = {style, re, text, usercssData};
|
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;
|
|
||||||
}
|
|
||||||
parseVar(state);
|
|
||||||
} else {
|
|
||||||
parseStringToEnd(state);
|
|
||||||
usercssData[key] = state.value;
|
|
||||||
}
|
|
||||||
let value = state.value;
|
|
||||||
if (key === 'version') {
|
|
||||||
value = usercssData[key] = normalizeVersion(value);
|
|
||||||
validateVersion(value);
|
|
||||||
}
|
|
||||||
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 {
|
return backgroundWorker.parseUsercssMeta(match[0], match.index)
|
||||||
doParse();
|
.catch(err => {
|
||||||
} catch (e) {
|
if (err.code) {
|
||||||
// the source code string offset
|
const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
|
||||||
e.index = metaIndex + state.re.lastIndex;
|
const message = chrome.i18n.getMessage(`meta_${err.code}`, args);
|
||||||
throw e;
|
if (message) {
|
||||||
|
err.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.maybeUSO && !usercssData.preprocessor) {
|
|
||||||
usercssData.preprocessor = 'uso';
|
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
validateStyle(style);
|
})
|
||||||
|
.then(({metadata}) => {
|
||||||
|
style.usercssData = metadata;
|
||||||
|
for (const [key, value = key] of Object.entries(GLOBAL_METAS)) {
|
||||||
|
style[value] = metadata[key];
|
||||||
|
}
|
||||||
return style;
|
return style;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeVersion(version) {
|
function drawList(items) {
|
||||||
// https://docs.npmjs.com/misc/semver#versions
|
return items.map(i => i.length === 1 ? JSON.stringify(i) : i).join(', ');
|
||||||
if (version[0] === 'v' || version[0] === '=') {
|
|
||||||
return version.slice(1);
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -518,136 +60,37 @@ const usercss = (() => {
|
||||||
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
|
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
|
||||||
*/
|
*/
|
||||||
function buildCode(style, allowErrors) {
|
function buildCode(style, allowErrors) {
|
||||||
const {usercssData: {preprocessor, vars}, sourceCode} = style;
|
const match = style.sourceCode.match(RX_META);
|
||||||
let builder;
|
return backgroundWorker.compileUsercss(
|
||||||
if (preprocessor) {
|
style.usercssData.preprocessor,
|
||||||
if (!BUILDER[preprocessor]) {
|
style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
|
||||||
return Promise.reject(chrome.i18n.getMessage('styleMetaErrorPreprocessor', preprocessor));
|
style.usercssData.vars
|
||||||
}
|
)
|
||||||
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,
|
|
||||||
}))
|
|
||||||
.then(({sections, errors}) => {
|
.then(({sections, errors}) => {
|
||||||
if (!errors.length) errors = false;
|
if (!errors.length) errors = false;
|
||||||
if (!sections.length || errors && !allowErrors) {
|
if (!sections.length || errors && !allowErrors) {
|
||||||
return Promise.reject(errors);
|
throw errors;
|
||||||
}
|
}
|
||||||
style.sections = sections;
|
style.sections = sections;
|
||||||
if (builder.postprocess) builder.postprocess(style.sections, sVars);
|
|
||||||
return allowErrors ? {style, errors} : style;
|
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) {
|
function assignVars(style, oldStyle) {
|
||||||
const {usercssData: {vars}} = style;
|
const {usercssData: {vars}} = style;
|
||||||
const {usercssData: {vars: oldVars}} = oldStyle;
|
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.
|
// 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)) {
|
for (const key of Object.keys(vars)) {
|
||||||
if (oldVars[key] && oldVars[key].value) {
|
if (oldVars[key] && oldVars[key].value) {
|
||||||
vars[key].value = 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};
|
|
||||||
})();
|
})();
|
||||||
|
|
98
js/worker-util.js
Normal file
98
js/worker-util.js
Normal file
|
@ -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));
|
||||||
|
}
|
||||||
|
})();
|
|
@ -172,7 +172,7 @@
|
||||||
<script src="manage/import-export.js" async></script>
|
<script src="manage/import-export.js" async></script>
|
||||||
<script src="manage/incremental-search.js" async></script>
|
<script src="manage/incremental-search.js" async></script>
|
||||||
<script src="msgbox/msgbox.js" async></script>
|
<script src="msgbox/msgbox.js" async></script>
|
||||||
<script src="js/sections-equal.js" async></script>
|
<script src="js/sections-util.js" async></script>
|
||||||
<script src="js/storage-util.js" async></script>
|
<script src="js/storage-util.js" async></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -184,7 +184,7 @@ function configDialog(style) {
|
||||||
.catch(errors => {
|
.catch(errors => {
|
||||||
const el = $('.config-error', messageBox.element) ||
|
const el = $('.config-error', messageBox.element) ||
|
||||||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
|
$('#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(() => {
|
.then(() => {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
"js/messaging.js",
|
"js/messaging.js",
|
||||||
"js/msg.js",
|
"js/msg.js",
|
||||||
"js/storage-util.js",
|
"js/storage-util.js",
|
||||||
"js/sections-equal.js",
|
"js/sections-util.js",
|
||||||
|
"js/worker-util.js",
|
||||||
"background/storage.js",
|
"background/storage.js",
|
||||||
"js/prefs.js",
|
"js/prefs.js",
|
||||||
"js/script-loader.js",
|
"js/script-loader.js",
|
||||||
|
@ -43,8 +44,7 @@
|
||||||
"background/search-db.js",
|
"background/search-db.js",
|
||||||
"background/update.js",
|
"background/update.js",
|
||||||
"background/openusercss-api.js",
|
"background/openusercss-api.js",
|
||||||
"vendor/semver-bundle/semver.js",
|
"vendor/semver-bundle/semver.js"
|
||||||
"vendor-overwrites/colorpicker/colorconverter.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
"stylelint-bundle": "^8.0.0",
|
"stylelint-bundle": "^8.0.0",
|
||||||
"stylus-lang-bundle": "^0.54.5",
|
"stylus-lang-bundle": "^0.54.5",
|
||||||
"updates": "^4.2.1",
|
"updates": "^4.2.1",
|
||||||
"web-ext": "^2.9.1"
|
"web-ext": "^2.9.1",
|
||||||
|
"usercss-meta": "^0.8.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint **/*.js --cache || exit 0",
|
"lint": "eslint **/*.js --cache || exit 0",
|
||||||
|
|
|
@ -28,6 +28,9 @@ const files = {
|
||||||
],
|
],
|
||||||
'stylus-lang-bundle': [
|
'stylus-lang-bundle': [
|
||||||
'stylus.min.js'
|
'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 pkg = await fs.readJson(`${root}/node_modules/${lib}/package.json`);
|
||||||
const file = `${root}/vendor/${lib}/README.md`;
|
const file = `${root}/vendor/${lib}/README.md`;
|
||||||
const txt = await fs.readFile(file, 'utf8');
|
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) {
|
function isFolder(fileOrFolder) {
|
||||||
|
|
|
@ -5505,3 +5505,5 @@ self.parserlib = (() => {
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
self.parserlib.css.Tokens[self.parserlib.css.Tokens.COMMENT].hide = false;
|
||||||
|
|
4
vendor/README.md
vendored
4
vendor/README.md
vendored
|
@ -9,7 +9,8 @@ Using this repo, run `npm install`... the latest versions of:
|
||||||
* `less` (https://github.com/less/less.js) is installed.
|
* `less` (https://github.com/less/less.js) is installed.
|
||||||
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
|
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
|
||||||
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
|
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
|
||||||
* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.<br><br>
|
* `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`.
|
* The necessary build tools are installed; see `devDependencies` in the `package.json`.
|
||||||
|
|
||||||
## Running the build script
|
## 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`.
|
* `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`.
|
* `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`.
|
* `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
|
## Creating the ZIP
|
||||||
|
|
||||||
|
|
22
vendor/usercss-meta/LICENCE
vendored
Normal file
22
vendor/usercss-meta/LICENCE
vendored
Normal file
|
@ -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.
|
5
vendor/usercss-meta/README.md
vendored
Normal file
5
vendor/usercss-meta/README.md
vendored
Normal file
|
@ -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
|
2
vendor/usercss-meta/usercss-meta.min.js
vendored
Normal file
2
vendor/usercss-meta/usercss-meta.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user