Rewrite parser, add uso preprocessor
This commit is contained in:
parent
21256e32f7
commit
eaf33afbe3
|
@ -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 => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
278
js/usercss.js
278
js/usercss.js
|
@ -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') {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user