Rewrite parser, add uso preprocessor

This commit is contained in:
eight 2017-09-15 13:40:04 +08:00
parent 21256e32f7
commit eaf33afbe3
3 changed files with 229 additions and 60 deletions

View File

@ -377,7 +377,7 @@ function filterUsercss(req) {
return buildMeta() return buildMeta()
.then(buildSection) .then(buildSection)
.then(decide) .then(decide)
.catch(err => ({status: 'error', error: err.message})); .catch(err => ({status: 'error', error: err.message || String(err)}));
function buildMeta() { function buildMeta() {
return new Promise(resolve => { return new Promise(resolve => {

View File

@ -18,6 +18,7 @@ body {
padding: 15px; padding: 15px;
border-right: 1px dashed #aaa; border-right: 1px dashed #aaa;
box-shadow: 0 0 50px -18px black; box-shadow: 0 0 50px -18px black;
overflow-wrap: break-word;
} }
.header h1:first-child { .header h1:first-child {
@ -99,6 +100,6 @@ button.install.installed {
.main { .main {
flex-grow: 1; flex-grow: 1;
overflow: auto; overflow: hidden;
overflow-wrap: break-word; overflow-wrap: break-word;
} }

View File

@ -5,10 +5,12 @@
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var usercss = (function () { var usercss = (function () {
const METAS = [ const METAS = [
'author', 'description', 'homepageURL', 'icon', 'license', 'name', 'author', 'advanced', 'description', 'homepageURL', 'icon', 'license', 'name',
'namespace', 'noframes', 'preprocessor', 'supportURL', 'var', 'version' 'namespace', 'noframes', 'preprocessor', 'supportURL', 'var', 'version'
]; ];
const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image'];
const BUILDER = { const BUILDER = {
default: { default: {
postprocess(sections, vars) { postprocess(sections, vars) {
@ -42,6 +44,45 @@ var usercss = (function () {
}) })
)); ));
} }
},
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') {
// eslint-disable-next-line no-use-before-define
const color = colorParser.parse(vars[name].value);
return `${color.r}, ${color.g}, ${color.b}`;
}
return null;
}
if (vars[name].type === 'dropdown') {
// prevent infinite recursion
pool.set('');
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);
});
}
}
} }
}; };
@ -104,65 +145,178 @@ var usercss = (function () {
return style; return style;
} }
function *parseMetas(source) { function parseWord(state, error) {
for (const line of source.split(/\r?\n/)) { const match = state.text.slice(state.re.lastIndex).match(/^([\w-]+)\s+/);
const match = line.match(/@(\w+)/);
if (!match) { if (!match) {
continue; throw new Error(error);
}
yield [match[1], line.slice(match.index + match[0].length).trim()];
} }
state.value = match[1];
state.re.lastIndex += match[0].length;
} }
function matchString(s) { function parseVar(state) {
const match = matchFollow(s, /^(?:\w+|(['"])(?:\\\1|.)*?\1)/);
match.value = match[1] ? match[0].slice(1, -1) : match[0];
return match;
}
function matchFollow(s, re) {
const match = s.match(re);
match.follow = s.slice(match.index + match[0].length).trim();
return match;
}
function parseVar(source) {
const result = { const result = {
type: null,
label: null, label: null,
name: null, name: null,
value: null, value: null,
default: null, default: null,
select: null options: null
}; };
{ parseWord(state, 'missing type');
// type & name result.type = state.type = state.value;
const match = matchFollow(source, /^([\w-]+)\s+([\w-]+)/); if (!META_VARS.includes(state.type)) {
([, result.type, result.name] = match); throw new Error(`unknown type: ${state.type}`);
source = match.follow;
} }
{ parseWord(state, 'missing name');
// label result.name = state.value;
const match = matchString(source);
result.label = match.value; parseString(state);
source = match.follow; result.label = state.value;
if (state.type === 'checkbox') {
const match = state.text.slice(state.re.lastIndex).match(/([01])\s+/);
if (!match) {
throw new Error('value must be 0 or 1');
}
state.re.lastIndex += match[0].length;
result.default = match[1];
} else if (state.type === 'select' || (state.type === 'image' && state.key === 'var')) {
parseJSON(state);
result.options = Object.keys(state.value).map(k => ({
label: k,
value: state.value[k]
}));
result.default = result.options[0].value;
} else if (state.type === 'dropdown' || state.type === 'image') {
if (state.text[state.re.lastIndex] !== '{') {
throw new Error('no open {');
}
result.options = [];
state.re.lastIndex++;
while (state.text[state.re.lastIndex] !== '}') {
const option = {};
parseStringUnquoted(state);
option.name = state.value;
parseString(state);
option.label = state.value;
if (state.type === 'dropdown') {
parseEOT(state);
} else {
parseString(state);
}
option.value = state.value;
result.options.push(option);
}
state.re.lastIndex++;
eatWhitespace(state);
result.default = result.options[0].value;
} else {
// text, color
parseStringToEnd(state);
result.default = state.value;
}
state.style.vars[result.name] = result;
} }
// select type has an additional field function parseEOT(state) {
if (result.type === 'select') { const match = state.text.slice(state.re.lastIndex).match(/^<<<EOT([\s\S]+?)EOT;/);
const match = matchString(source); 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 match = state.text.slice(state.re.lastIndex).match(/^[^"]*/);
state.re.lastIndex += match[0].length;
state.value = match[0].trim().replace(/\s+/g, '-');
}
function parseString(state) {
const match = state.text.slice(state.re.lastIndex).match(/^((['"])(?:\\\2|[^\n])*?\2|\w+)\s+/);
state.re.lastIndex += match[0].length;
state.value = unquote(match[1]);
}
function parseJSON(state) {
const result = looseJSONParse(state.text.slice(state.re.lastIndex));
state.re.lastIndex += result.length;
state.value = result.json;
}
function looseJSONParse(text) {
try { try {
result.select = JSON.parse(match.follow); return {
json: JSON.parse(text),
length: text.length
};
} catch (e) { } catch (e) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelect', e.message)); let pos;
{
const match = e.message.match(/after json data at line (\d+) column (\d+)/i);
if (match) {
const [, line, column] = match.map(Number);
pos = indexFromLine(text, line - 1) + column - 1;
}
}
if (pos === undefined) {
const match = e.message.match(/at position (\d+)/);
if (match) {
pos = Number(match[1]);
}
}
if (pos) {
try {
return {
json: JSON.parse(text.slice(0, pos)),
length: pos
};
} catch (e2) {}
}
throw e;
} }
source = match.value;
} }
result.default = source; function indexFromLine(text, line) {
if (line === 0) {
return 0;
}
const re = /\r?\n/g;
let i = 1;
while (re.exec(text)) {
if (i++ === line) {
return re.lastIndex;
}
}
throw new Error('out of range');
}
return result; function eatWhitespace(state) {
const match = state.text.slice(state.re.lastIndex).match(/\s*/);
state.re.lastIndex += match[0].length;
}
function parseStringToEnd(state) {
const match = state.text.slice(state.re.lastIndex).match(/.+/);
state.value = unquote(match[0].trim());
state.re.lastIndex += match[0].length;
}
function unquote(s) {
const match = s.match(/^(['"])(.*)\1$/);
if (match) {
return match[2];
}
return s;
} }
function _buildMeta(source) { function _buildMeta(source) {
@ -179,22 +333,34 @@ var usercss = (function () {
noframes: false noframes: false
}; };
const metaSource = getMetaSource(source); const text = getMetaSource(source);
const re = /@(\w+)\s+/mg;
const state = {style, re, text};
for (const [key, value] of parseMetas(metaSource)) { let match;
if (!METAS.includes(key)) { while ((match = re.exec(text))) {
state.key = match[1];
if (!METAS.includes(state.key)) {
continue; continue;
} }
if (key === 'noframes') { if (state.key === 'noframes') {
style.noframes = true; style.noframes = true;
} else if (key === 'var') { } else if (state.key === 'var' || state.key === 'advanced') {
const va = parseVar(value); if (state.key === 'advanced') {
style.vars[va.name] = va; state.maybeUSO = true;
} else if (key === 'homepageURL') {
style.url = value;
} else {
style[key] = value;
} }
parseVar(state);
} else {
parseStringToEnd(state);
if (state.key === 'homepageURL') {
style.url = state.value;
} else {
style[state.key] = state.value;
}
}
}
if (state.maybeUSO && !style.preprocessor) {
style.preprocessor = 'uso';
} }
return style; return style;
@ -239,10 +405,10 @@ var usercss = (function () {
// need to test each va's default value. // need to test each va's default value.
return Object.keys(vars).reduce((output, key) => { return Object.keys(vars).reduce((output, key) => {
const va = vars[key]; const va = vars[key];
output[key] = { output[key] = Object.assign({}, va, {
value: va.value === null || va.value === undefined ? value: va.value === null || va.value === undefined ?
va.default : va.value va.default : va.value,
}; });
return output; return output;
}, {}); }, {});
} }
@ -278,8 +444,10 @@ var usercss = (function () {
} }
function validVar(va, value = 'default') { function validVar(va, value = 'default') {
if (va.type === 'select' && !va.select[va[value]]) { if (va.type === 'select' || va.type === 'dropdown') {
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectMissingKey', va[value])); if (va.options.every(o => o.value !== va[value])) {
throw new Error(chrome.i18n.getMessage('invalid select value'));
}
} else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) { } else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox')); throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox'));
} else if (va.type === 'color') { } else if (va.type === 'color') {