Merge branch 'dev-usercss-meta' into dev-exclusions

This commit is contained in:
eight 2018-10-11 19:49:37 +08:00
commit bd4a453f45
29 changed files with 793 additions and 872 deletions

View File

@ -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"

View 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;
}

View File

@ -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,

View File

@ -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));
};

View File

@ -153,24 +153,27 @@
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 =>
const {usercssData: {version}} = style; usercss.buildMeta(text)
const {usercssData: {version: newVersion}} = json; .then(json => {
switch (Math.sign(semverCompare(version, newVersion))) { const {usercssData: {version}} = style;
case 0: const {usercssData: {version: newVersion}} = json;
// re-install is invalid in a soft upgrade switch (Math.sign(semverCompare(version, newVersion))) {
if (!ignoreDigest) { case 0:
const sameCode = text === style.sourceCode; // re-install is invalid in a soft upgrade
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); if (!ignoreDigest) {
} const sameCode = text === style.sourceCode;
break; return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
case 1: }
// downgrade is always invalid break;
return Promise.reject(STATES.ERROR_VERSION); case 1:
} // downgrade is always invalid
return usercss.buildCode(json); return Promise.reject(STATES.ERROR_VERSION);
}); }
return usercss.buildCode(json);
})
);
} }
function maybeSave(json = {}) { function maybeSave(json = {}) {

View File

@ -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);
} }

View File

@ -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>

View File

@ -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},

View File

@ -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

View File

@ -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);
}

View File

@ -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({}, { createAPI({
get: (target, prop) => csslint: (code, config) => {
(...args) => { loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
if (!worker) { return CSSLint.verify(code, config).messages
worker = createWorker(); .map(m => Object.assign(m, {rule: {id: m.rule.id}}));
} },
return worker.invoke(prop, args); 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() { function getStylelintRules() {
let id = 0; loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
const pendingResponse = new Map(); const stylelint = require('stylelint');
const worker = new Worker('/edit/editor-worker-body.js'); const options = {};
worker.onmessage = e => { const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
const message = e.data; const rxString = /"([-\w\s]{3,}?)"/g;
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data); for (const id of Object.keys(stylelint.rules)) {
pendingResponse.delete(message.id); const ruleCode = String(stylelint.rules[id]);
}; const sets = [];
return {invoke}; let m, mStr;
while ((m = rxPossible.exec(ruleCode))) {
function invoke(action, args) { const possible = m[1];
return new Promise((resolve, reject) => { const set = [];
pendingResponse.set(id, {resolve, reject}); while ((mStr = rxString.exec(possible))) {
worker.postMessage({ const s = mStr[1];
id, if (s.includes(' ')) {
action, set.push(...s.split(/\s+/));
args } else {
}); set.push(s);
id++; }
}); }
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;
}

View File

@ -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);
}
} }
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]; meta = match[0];
metaIndex = match.index; 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; return cache;
}); });
}); });

View File

@ -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) {

View File

@ -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})`;

View File

@ -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
View 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;
}
})();

View File

@ -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

View File

@ -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('');
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 RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
const RX_NUMBER = /-?\d+(\.\d+)?\s*/y; const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
const RX_WHITESPACE = /\s*/y; return {buildMeta, buildCode, assignVars};
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() { return backgroundWorker.parseUsercssMeta(match[0], match.index)
let match; .catch(err => {
while ((match = re.exec(text))) { if (err.code) {
const key = state.key = match[1]; const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
const route = KNOWN_META.get(key); const message = chrome.i18n.getMessage(`meta_${err.code}`, args);
if (route === undefined) { if (message) {
continue; err.message = message;
}
if (key === 'var' || key === 'advanced') {
if (key === 'advanced') {
state.maybeUSO = true;
} }
parseVar(state);
} else {
parseStringToEnd(state);
usercssData[key] = state.value;
} }
let value = state.value; throw err;
if (key === 'version') { })
value = usercssData[key] = normalizeVersion(value); .then(({metadata}) => {
validateVersion(value); style.usercssData = metadata;
for (const [key, value = key] of Object.entries(GLOBAL_METAS)) {
style[value] = metadata[key];
} }
if (META_URLS.includes(key)) { return style;
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;
} }
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
View 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));
}
})();

View File

@ -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>

View File

@ -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;

View File

@ -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": {

View File

@ -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",

View File

@ -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) {

View File

@ -5505,3 +5505,5 @@ self.parserlib = (() => {
//endregion //endregion
})(); })();
self.parserlib.css.Tokens[self.parserlib.css.Tokens.COMMENT].hide = false;

4
vendor/README.md vendored
View File

@ -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
View 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
View 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

File diff suppressed because one or more lines are too long