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()
.then(buildSection)
.then(decide)
.catch(err => ({status: 'error', error: err.message}));
.catch(err => ({status: 'error', error: err.message || String(err)}));
function buildMeta() {
return new Promise(resolve => {

View File

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

View File

@ -5,10 +5,12 @@
// eslint-disable-next-line no-var
var usercss = (function () {
const METAS = [
'author', 'description', 'homepageURL', 'icon', 'license', 'name',
'author', 'advanced', 'description', 'homepageURL', 'icon', 'license', 'name',
'namespace', 'noframes', 'preprocessor', 'supportURL', 'var', 'version'
];
const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image'];
const BUILDER = {
default: {
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;
}
function *parseMetas(source) {
for (const line of source.split(/\r?\n/)) {
const match = line.match(/@(\w+)/);
if (!match) {
continue;
}
yield [match[1], line.slice(match.index + match[0].length).trim()];
function parseWord(state, error) {
const match = state.text.slice(state.re.lastIndex).match(/^([\w-]+)\s+/);
if (!match) {
throw new Error(error);
}
state.value = match[1];
state.re.lastIndex += match[0].length;
}
function matchString(s) {
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) {
function parseVar(state) {
const result = {
type: null,
label: null,
name: null,
value: null,
default: null,
select: null
options: null
};
{
// type & name
const match = matchFollow(source, /^([\w-]+)\s+([\w-]+)/);
([, result.type, result.name] = match);
source = match.follow;
parseWord(state, 'missing type');
result.type = state.type = state.value;
if (!META_VARS.includes(state.type)) {
throw new Error(`unknown type: ${state.type}`);
}
{
// label
const match = matchString(source);
result.label = match.value;
source = match.follow;
}
parseWord(state, 'missing name');
result.name = state.value;
// select type has an additional field
if (result.type === 'select') {
const match = matchString(source);
try {
result.select = JSON.parse(match.follow);
} catch (e) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelect', e.message));
parseString(state);
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');
}
source = match.value;
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;
}
result.default = source;
function parseEOT(state) {
const match = state.text.slice(state.re.lastIndex).match(/^<<<EOT([\s\S]+?)EOT;/);
if (!match) {
throw new Error('missing EOT');
}
state.re.lastIndex += match[0].length;
state.value = match[1].trim().replace(/\*\\\//g, '*/');
eatWhitespace(state);
}
return result;
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 {
return {
json: JSON.parse(text),
length: text.length
};
} catch (e) {
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;
}
}
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');
}
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) {
@ -179,23 +333,35 @@ var usercss = (function () {
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)) {
if (!METAS.includes(key)) {
let match;
while ((match = re.exec(text))) {
state.key = match[1];
if (!METAS.includes(state.key)) {
continue;
}
if (key === 'noframes') {
if (state.key === 'noframes') {
style.noframes = true;
} else if (key === 'var') {
const va = parseVar(value);
style.vars[va.name] = va;
} else if (key === 'homepageURL') {
style.url = value;
} else if (state.key === 'var' || state.key === 'advanced') {
if (state.key === 'advanced') {
state.maybeUSO = true;
}
parseVar(state);
} else {
style[key] = value;
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;
}
@ -239,10 +405,10 @@ var usercss = (function () {
// need to test each va's default value.
return Object.keys(vars).reduce((output, key) => {
const va = vars[key];
output[key] = {
output[key] = Object.assign({}, va, {
value: va.value === null || va.value === undefined ?
va.default : va.value
};
va.default : va.value,
});
return output;
}, {});
}
@ -278,8 +444,10 @@ var usercss = (function () {
}
function validVar(va, value = 'default') {
if (va.type === 'select' && !va.select[va[value]]) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectMissingKey', va[value]));
if (va.type === 'select' || va.type === 'dropdown') {
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])) {
throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox'));
} else if (va.type === 'color') {