From eaf33afbe3af23d3626a448bab5128b81e95c603 Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 15 Sep 2017 13:40:04 +0800 Subject: [PATCH] Rewrite parser, add uso preprocessor --- background/storage.js | 2 +- content/install-user-css.css | 3 +- js/usercss.js | 284 ++++++++++++++++++++++++++++------- 3 files changed, 229 insertions(+), 60 deletions(-) diff --git a/background/storage.js b/background/storage.js index bad07c81..af65afd4 100644 --- a/background/storage.js +++ b/background/storage.js @@ -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 => { diff --git a/content/install-user-css.css b/content/install-user-css.css index d97b915e..9de92ac3 100644 --- a/content/install-user-css.css +++ b/content/install-user-css.css @@ -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; } diff --git a/js/usercss.js b/js/usercss.js index 03eeb3bc..2841efd5 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -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(/^<< { 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') {