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",
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
},
"meta_invalidCheckboxDefault": {
"message": "Invalid @var checkbox: value must be 0 or 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"meta_invalidColor": {
"message": "Invalid @var color: $color$ is not a color",
"description": "Error displayed when the value of @var color is invalid",
"placeholders": {
"color": {
"content": "$1"
}
}
},
"meta_invalidRange": {
"message": "Invalid @var $type$: value must be a number or an array",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeMultipleUnits": {
"message": "Invalid @var $type$: multiple units are defined",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeTooManyValues": {
"message": "Invalid @var $type$: the array contains too many items",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeValue": {
"message": "Invalid @var $type$: items in the array must be number, string, or null",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeDefault": {
"message": "Invalid @var $type$: default value is null",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeMin": {
"message": "Invalid @var $type$: default value is lower than the minimum",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeMax": {
"message": "Invalid @var $type$: default value is larger than the maximum",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidRangeStep": {
"message": "Invalid @var $type$: default value is not a mutiple of the step",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"meta_invalidSelectEmptyOptions": {
"message": "Invalid @var select: options list is empty",
"description": "Error displayed when the value of @var select is invalid"
},
"meta_invalidSelectMultipleDefaults": {
"message": "Invalid @var select: multiple default options are defined",
"description": "Error displayed when the value of @var select is invalid"
},
"meta_invalidSelectValueMismatch": {
"message": "Invalid @var select: value doesn't exist in the option list",
"description": "Error displayed when the value of @var select is invalid"
},
"meta_invalidURLProtocol": {
"message": "Invalid URL protocol. Only http and https are allowed: $protocol$",
"description": "Error displayed when the protocol of the URL is invalid",
"placeholders": {
"protocol": {
"content": "$1"
}
}
},
"meta_invalidVersion": {
"message": "Invalid version number. The value doesn't match SemVer pattern: $version$",
"description": "Error displayed when @version is invalid",
"placeholders": {
"version": {
"content": "$1"
}
}
},
"meta_invalidNumber": {
"message": "Expect a number",
"description": "Error displayed when the value is expected to be a number"
},
"meta_invalidString": {
"message": "Expect a quoted string",
"description": "Error displayed when the value is expected to be a quoted string"
},
"meta_invalidWord": {
"message": "Expect a word",
"description": "Error displayed when the value is expected to be a word"
},
"meta_missingChar": {
"message": "Expect characters: $chars$",
"description": "Error displayed when the value is expected to be some characters",
"placeholders": {
"chars": {
"content": "$1"
}
}
},
"meta_missingEOT": {
"message": "Expect EOT data",
"description": "Error displayed when the value is expected to be an EOT list"
},
"meta_missingMandatory": {
"message": "Missing mandatory metadata: $keys$",
"description": "Error displayed when mandatory keys are missing",
"placeholders": {
"keys": {
"content": "$1"
}
}
},
"meta_unknownJSONLiteral": {
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
"description": "Error displayed when JSON value is invalid",
"placeholders": {
"literal": {
"content": "$1"
}
}
},
"meta_unknownMeta": {
"message": "Unknown metadata: $key$",
"description": "Error displayed when unknown metadata is parsed",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"meta_unknownVarType": {
"message": "Unknown @$varkey$ type: $vartype$",
"description": "Error displayed when unknown variable type is parsed",
"placeholders": {
"varkey": {
"content": "$1"
},
"vartype": {
"content": "$2"
}
}
},
"meta_unknownPreprocessor": {
"message": "Unknown @preprocessor: $preprocessor$",
"description": "Error displayed when unknown @preprocessor is parsed",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"noStylesForSite": {
"message": "No styles installed for this site.",
"description": "Text displayed when no styles are installed for the current site"
@ -1087,50 +1275,6 @@
},
"description": "Confirmation when re-installing a style"
},
"styleMetaErrorCheckbox": {
"message": "Invalid @var checkbox: value must be 0 or 1",
"description": "Error displayed when the value of @var checkbox is invalid"
},
"styleMetaErrorColor": {
"message": "$color$ is not a valid color",
"placeholders": {
"color": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var color is invalid"
},
"styleMetaErrorRangeOrNumber": {
"message": "Invalid @var $type$: value must be an array containing at least one number at index zero",
"description": "Error displayed when the value of @var number or @var range is invalid",
"placeholders": {
"type": {
"content": "$1"
}
}
},
"styleMetaErrorPreprocessor": {
"message": "Unsupported @preprocessor: $preprocessor$",
"placeholders": {
"preprocessor": {
"content": "$1"
}
},
"description": "Error displayed when the value of @preprocessor is not supported"
},
"styleMetaErrorSelectValueMismatch": {
"message": "Invalid @select: value doesn't exist in the list",
"description": "Error displayed when the value of @select is invalid"
},
"styleMissingMeta": {
"message": "Missing metadata @$key$",
"placeholders": {
"key": {
"content": "$1"
}
},
"description": "Error displayed when a mandatory metadata is missing"
},
"styleMissingName": {
"message": "Enter a name",
"description": "Error displayed when user saves without providing a name"

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
openEditor debounce URLS ignoreChromeError queryTabs getTab
usercss styleManager db msg navigatorUtil iconUtil
*/
usercss styleManager db msg navigatorUtil iconUtil */
'use strict';
// eslint-disable-next-line no-var
var backgroundWorker = workerUtil.createWorker({
url: '/background/background-worker.js'
});
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
getSectionsByUrl: styleManager.getSectionsByUrl,
getSectionsById: styleManager.getSectionsById,
@ -26,7 +30,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
return download(msg.url, msg);
},
parseCss({code}) {
return usercss.invokeWorker({action: 'parse', code});
return backgroundWorker.parseMozFormat({code});
},
getPrefs: prefs.getAll,

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,8 +153,10 @@
function maybeUpdateUsercss() {
// TODO: when sourceCode is > 100kB use http range request(s) for version check
return download(style.updateUrl).then(text => {
const json = usercss.buildMeta(text);
return download(style.updateUrl)
.then(text =>
usercss.buildMeta(text)
.then(json => {
const {usercssData: {version}} = style;
const {usercssData: {version: newVersion}} = json;
switch (Math.sign(semverCompare(version, newVersion))) {
@ -170,7 +172,8 @@
return Promise.reject(STATES.ERROR_VERSION);
}
return usercss.buildCode(json);
});
})
);
}
function maybeSave(json = {}) {

View File

@ -42,14 +42,13 @@
if (style.usercssData) {
return Promise.resolve(style);
}
try {
const {sourceCode} = style;
// allow sourceCode to be normalized
const {sourceCode} = style;
delete style.sourceCode;
return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style));
} catch (e) {
return Promise.reject(e);
}
return usercss.buildMeta(sourceCode)
.then(newStyle => Object.assign(newStyle, style));
}
function assignVars(style) {
@ -62,7 +61,8 @@
style.id = dup.id;
if (style.reason !== 'config') {
// preserve style.vars during update
usercss.assignVars(style, dup);
return usercss.assignVars(style, dup)
.then(() => style);
}
}
return style;
@ -95,7 +95,8 @@
function doBuild(style) {
if (vars) {
const oldStyle = {usercssData: {vars}};
usercss.assignVars(style, oldStyle);
return usercss.assignVars(style, oldStyle)
.then(() => usercss.buildCode(style));
}
return usercss.buildCode(style);
}

View File

@ -70,6 +70,7 @@
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="js/msg.js"></script>
<script src="js/worker-util.js"></script>
<script src="content/apply.js"></script>
@ -81,8 +82,6 @@
<link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script>
<script src="edit/editor-worker.js"></script>
<script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script>
<script src="edit/live-preview.js"></script>

View File

@ -351,8 +351,9 @@ CodeMirror.hint && (() => {
}
// USO vars in usercss mode editor
const list = Object.keys(editor.getStyle().usercssData.vars)
.filter(name => name.startsWith(leftPart));
const vars = editor.getStyle().usercssData.vars;
const list = vars ?
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
return {
list,
from: {line, ch: prev},

View File

@ -2,11 +2,14 @@
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
closeCurrentTab messageBox debounce
beautify
moveFocus msg createSectionsEditor rerouteHotkeys
*/
moveFocus msg createSectionsEditor rerouteHotkeys */
/* exported showCodeMirrorPopup */
'use strict';
const editorWorker = workerUtil.createWorker({
url: '/edit/editor-worker.js'
});
let saveSizeOnClose;
// direct & reverse mapping of @-moz-document keywords and internal property names

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 */
'use strict';
// eslint-disable-next-line no-var
var editorWorker = (() => {
let worker;
return new Proxy({}, {
get: (target, prop) =>
(...args) => {
if (!worker) {
worker = createWorker();
}
return worker.invoke(prop, args);
}
});
importScripts('/js/worker-util.js');
const {createAPI, loadScript} = workerUtil;
function createWorker() {
let id = 0;
const pendingResponse = new Map();
const worker = new Worker('/edit/editor-worker-body.js');
worker.onmessage = e => {
const message = e.data;
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
pendingResponse.delete(message.id);
};
return {invoke};
createAPI({
csslint: (code, config) => {
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
return CSSLint.verify(code, config).messages
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
},
stylelint: (code, config) => {
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
return require('stylelint').lint({code, config});
},
metalint: code => {
loadScript(
'/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js'
);
const result = metaParser.lint(code);
// extract needed info
result.errors = result.errors.map(err =>
({
code: err.code,
args: err.args,
message: err.message,
index: err.index
})
);
return result;
},
getStylelintRules,
getCsslintRules
});
function invoke(action, args) {
return new Promise((resolve, reject) => {
pendingResponse.set(id, {resolve, reject});
worker.postMessage({
id,
action,
args
});
id++;
});
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;
}

View File

@ -1,4 +1,4 @@
/* global linter API */
/* global linter API editorWorker */
/* exported createMetaCompiler */
'use strict';
@ -19,25 +19,23 @@ function createMetaCompiler(cm) {
if (match[0] === meta && match.index === metaIndex) {
return cache;
}
return API.parseUsercss({sourceCode: match[0], metaOnly: true})
.then(result => result.usercssData)
.then(result => {
return editorWorker.metalint(match[0])
.then(({metadata, errors}) => {
if (errors.every(err => err.code === 'unknownMeta')) {
for (const cb of updateListeners) {
cb(result);
cb(metadata);
}
meta = match[0];
metaIndex = match.index;
cache = [];
return cache;
}, err => {
meta = match[0];
metaIndex = match.index;
cache = [{
}
cache = errors.map(err =>
({
from: cm.posFromIndex((err.index || 0) + match.index),
to: cm.posFromIndex((err.index || 0) + match.index),
message: err.message,
severity: 'error'
}];
message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message,
severity: err.code === 'unknownMeta' ? 'warning' : 'error'
})
);
meta = match[0];
metaIndex = match.index;
return cache;
});
});

View File

@ -432,7 +432,7 @@ function createSectionsEditor(style) {
function doImport({replaceOldStyle = false}) {
lockPageUI(true);
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
API.parseCss({code: popup.codebox.getValue().trim()})
.then(({sections, errors}) => {
// shouldn't happen but just in case
if (!sections.length || errors.length) {

View File

@ -249,7 +249,7 @@ function createSourceEditor(style) {
.then(replaceStyle)
.catch(err => {
if (err.handled) return;
if (err.message === t('styleMissingMeta', 'name')) {
if (err.code === 'missingMandatory' && err.args.includes('name')) {
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
chromeSync.setLZValue('usercssTemplate', code)
.then(() => chromeSync.getLZValue('usercssTemplate'))
@ -258,7 +258,7 @@ function createSourceEditor(style) {
}
const contents = Array.isArray(err) ?
$create('pre', err.join('\n')) :
[String(err)];
[err.message || String(err)];
if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index);
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;

View File

@ -241,7 +241,7 @@
const contents = Array.isArray(err) ?
[$create('pre', err.join('\n'))] :
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
if (Number.isInteger(err.index)) {
if (Number.isInteger(err.index) && typeof contents[0] === 'string') {
const pos = cm.posFromIndex(err.index);
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
contents.push($create('pre', drawLinePointer(pos)));

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 */
'use strict';
const RX_NAMESPACE = /\s*(@namespace\s+(?:\S+\s+)?url\(http:\/\/.*?\);)\s*/g;
const RX_CHARSET = /\s*@charset\s+(['"]).*?\1\s*;\s*/g;
const RX_CSS_COMMENTS = /\/\*[\s\S]*?(?:\*\/|$)/g;
function styleCodeEmpty(code) {
// Collect the global section if it's not empty, not comment-only, not namespace-only.
const cmtOpen = code && code.indexOf('/*');
if (cmtOpen >= 0) {
const cmtCloseLast = code.lastIndexOf('*/');
if (cmtCloseLast < 0) {
code = code.substr(0, cmtOpen);
} else {
code = code.substr(0, cmtOpen) +
code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
code.substr(cmtCloseLast + 2);
}
}
if (!code || !code.trim()) return true;
if (code.includes('@namespace')) code = code.replace(RX_NAMESPACE, '').trim();
if (code.includes('@charset')) code = code.replace(RX_CHARSET, '').trim();
return !code;
}
/**
* @param {Style} a - first style object
* @param {Style} b - second style object

View File

@ -1,514 +1,56 @@
/* global loadScript semverCompare colorConverter styleCodeEmpty */
/* global loadScript semverCompare colorConverter styleCodeEmpty backgroundWorker */
/* exported usercss */
'use strict';
const usercss = (() => {
// true = global
// false or 0 = private
// <string> = global key name
// <function> = (style, newValue)
const KNOWN_META = new Map([
['author', true],
['advanced', 0],
['description', true],
['homepageURL', 'url'],
['icon', 0],
['license', 0],
['name', true],
['namespace', 0],
//['noframes', 0],
['preprocessor', 0],
['supportURL', 0],
['updateURL', (style, newValue) => {
// always preserve locally installed style's updateUrl
if (!/^file:/.test(style.updateUrl)) {
style.updateUrl = newValue;
}
}],
['var', 0],
['version', 0],
]);
const MANDATORY_META = ['name', 'namespace', 'version'];
const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image', 'number', 'range'];
const META_URLS = [...KNOWN_META.keys()].filter(k => k.endsWith('URL'));
const BUILDER = {
default: {
postprocess(sections, vars) {
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
if (!varDef) return;
varDef = ':root {\n' + varDef + '}\n';
for (const section of sections) {
if (!styleCodeEmpty(section.code)) {
section.code = varDef + section.code;
}
}
}
},
stylus: {
preprocess(source, vars) {
return loadScript('/vendor/stylus-lang-bundle/stylus.min.js').then(() => (
new Promise((resolve, reject) => {
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
window.stylus(varDef + source).render((err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
})
));
}
},
less: {
preprocess(source, vars) {
window.less = window.less || {
logLevel: 0,
useFileCache: false,
const GLOBAL_METAS = {
author: undefined,
description: undefined,
homepageURL: 'url',
// updateURL: 'updateUrl',
name: undefined,
};
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_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;
}
const RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
return {buildMeta, buildCode, assignVars};
function buildMeta(sourceCode) {
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
const usercssData = {
vars: {}
};
const style = {
reason: 'install',
enabled: true,
sourceCode,
sections: [],
usercssData
sections: []
};
const {text, index: metaIndex} = getMetaSource(sourceCode);
const re = /@(\w+)[ \t\xA0]*/mg;
const state = {style, re, text, usercssData};
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;
}
}
}
const match = sourceCode.match(RX_META);
if (!match) {
throw new Error('can not find metadata');
}
try {
doParse();
} catch (e) {
// the source code string offset
e.index = metaIndex + state.re.lastIndex;
throw e;
return backgroundWorker.parseUsercssMeta(match[0], match.index)
.catch(err => {
if (err.code) {
const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
const message = chrome.i18n.getMessage(`meta_${err.code}`, args);
if (message) {
err.message = message;
}
if (state.maybeUSO && !usercssData.preprocessor) {
usercssData.preprocessor = 'uso';
}
validateStyle(style);
throw err;
})
.then(({metadata}) => {
style.usercssData = metadata;
for (const [key, value = key] of Object.entries(GLOBAL_METAS)) {
style[value] = metadata[key];
}
return style;
});
}
function normalizeVersion(version) {
// https://docs.npmjs.com/misc/semver#versions
if (version[0] === 'v' || version[0] === '=') {
return version.slice(1);
}
return version;
function drawList(items) {
return items.map(i => i.length === 1 ? JSON.stringify(i) : i).join(', ');
}
/**
@ -518,136 +60,37 @@ const usercss = (() => {
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
*/
function buildCode(style, allowErrors) {
const {usercssData: {preprocessor, vars}, sourceCode} = style;
let builder;
if (preprocessor) {
if (!BUILDER[preprocessor]) {
return Promise.reject(chrome.i18n.getMessage('styleMetaErrorPreprocessor', preprocessor));
}
builder = BUILDER[preprocessor];
} else {
builder = BUILDER.default;
}
const sVars = simpleVars(vars);
return (
Promise.resolve(
builder.preprocess && builder.preprocess(sourceCode, sVars) ||
sourceCode)
.then(mozStyle => invokeWorker({
action: 'parse',
styleId: style.id,
code: mozStyle,
}))
const match = style.sourceCode.match(RX_META);
return backgroundWorker.compileUsercss(
style.usercssData.preprocessor,
style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
style.usercssData.vars
)
.then(({sections, errors}) => {
if (!errors.length) errors = false;
if (!sections.length || errors && !allowErrors) {
return Promise.reject(errors);
throw errors;
}
style.sections = sections;
if (builder.postprocess) builder.postprocess(style.sections, sVars);
return allowErrors ? {style, errors} : style;
}));
}
function simpleVars(vars) {
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
// need to test each va's default value.
return Object.keys(vars).reduce((output, key) => {
const va = vars[key];
output[key] = Object.assign({}, va, {
value: va.value === null || va.value === undefined ?
getVarValue(va, 'default') : getVarValue(va, 'value')
});
return output;
}, {});
}
function getVarValue(va, prop) {
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
// TODO: handle customized image
return va.options.find(o => o.name === va[prop]).value;
}
if ((va.type === 'number' || va.type === 'range') && va.units) {
return va[prop] + va.units;
}
return va[prop];
}
function validateStyle({usercssData: data}) {
for (const prop of MANDATORY_META) {
if (!data[prop]) {
throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop));
}
}
validateVersion(data.version);
META_URLS.forEach(k => validateUrl(k, data[k]));
Object.keys(data.vars).forEach(k => validateVar(data.vars[k]));
}
function validateVersion(version) {
semverCompare(version, '0.0.0');
}
function validateUrl(key, url) {
if (!url) {
return;
}
url = new URL(url);
if (!/^https?:/.test(url.protocol)) {
throw new Error(`${url.protocol} is not a valid protocol in ${key}`);
}
}
function validateVar(va, value = 'default') {
if (va.type === 'select' || va.type === 'dropdown') {
if (va.options.every(o => o.name !== va[value])) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectValueMismatch'));
}
} else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox'));
} else if (va.type === 'color') {
va[value] = colorConverter.format(colorConverter.parse(va[value]), 'rgb');
} else if ((va.type === 'number' || va.type === 'range') && typeof va[value] !== 'number') {
throw new Error(chrome.i18n.getMessage('styleMetaErrorRangeOrNumber', va.type));
}
}
function assignVars(style, oldStyle) {
const {usercssData: {vars}} = style;
const {usercssData: {vars: oldVars}} = oldStyle;
if (!vars || !oldVars) {
return Promise.resolve();
}
// The type of var might be changed during the update. Set value to null if the value is invalid.
for (const key of Object.keys(vars)) {
if (oldVars[key] && oldVars[key].value) {
vars[key].value = oldVars[key].value;
try {
validateVar(vars[key], 'value');
} catch (e) {
vars[key].value = null;
}
}
}
}
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 backgroundWorker.nullifyInvalidVars(vars)
.then(vars => {
style.usercssData.vars = vars;
});
}
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/incremental-search.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>
</head>

View File

@ -184,7 +184,7 @@ function configDialog(style) {
.catch(errors => {
const el = $('.config-error', messageBox.element) ||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors;
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors.message || String(errors);
})
.then(() => {
saving = false;

View File

@ -27,7 +27,8 @@
"js/messaging.js",
"js/msg.js",
"js/storage-util.js",
"js/sections-equal.js",
"js/sections-util.js",
"js/worker-util.js",
"background/storage.js",
"js/prefs.js",
"js/script-loader.js",
@ -43,8 +44,7 @@
"background/search-db.js",
"background/update.js",
"background/openusercss-api.js",
"vendor/semver-bundle/semver.js",
"vendor-overwrites/colorpicker/colorconverter.js"
"vendor/semver-bundle/semver.js"
]
},
"commands": {

View File

@ -18,7 +18,8 @@
"stylelint-bundle": "^8.0.0",
"stylus-lang-bundle": "^0.54.5",
"updates": "^4.2.1",
"web-ext": "^2.9.1"
"web-ext": "^2.9.1",
"usercss-meta": "^0.8.1"
},
"scripts": {
"lint": "eslint **/*.js --cache || exit 0",

View File

@ -28,6 +28,9 @@ const files = {
],
'stylus-lang-bundle': [
'stylus.min.js'
],
'usercss-meta': [
'dist/usercss-meta.min.js → usercss-meta.min.js'
]
};
@ -35,7 +38,7 @@ async function updateReadme(lib) {
const pkg = await fs.readJson(`${root}/node_modules/${lib}/package.json`);
const file = `${root}/vendor/${lib}/README.md`;
const txt = await fs.readFile(file, 'utf8');
return fs.writeFile(file, txt.replace(/\bv[\d.]+[-\w]*\b/g, `v${pkg.version}`));
return fs.writeFile(file, txt.replace(/\b([v@])[\d.]+[-\w]*\b/g, `$1${pkg.version}`));
}
function isFolder(fileOrFolder) {

View File

@ -5505,3 +5505,5 @@ self.parserlib = (() => {
//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.
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.<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`.
## Running the build script
@ -24,6 +25,7 @@ The following changes are made:
* `lz-string-unsafe`: The compressed `lz-string-unsafe.min.js` file is copied directly into `vendor/lz-string-unsafe`.
* `semver-bundle`: The `dist/semver.js` file is copied directly into `vendor/semver`.
* `stylus-lang-bundle`: The `stylus.min.js` file is copied directly into `vendor/stylus-lang-bundle`.
* `usercss-meta`: The `dist/usercss-meta.min.js` file is copied directly into `vendor/usercss-meta`.
## Creating the ZIP

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