From 7a24547e09984327fdee20426412d0744150413a Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 00:01:18 +0800 Subject: [PATCH 01/38] Add: usercss-meta --- package.json | 3 ++- tools/update-libraries.js | 3 +++ vendor/README.md | 4 +++- vendor/usercss-meta/LICENCE | 22 ++++++++++++++++++++++ vendor/usercss-meta/README.md | 5 +++++ vendor/usercss-meta/usercss-meta.min.js | 2 ++ 6 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 vendor/usercss-meta/LICENCE create mode 100644 vendor/usercss-meta/README.md create mode 100644 vendor/usercss-meta/usercss-meta.min.js diff --git a/package.json b/package.json index 8ebd7796..d3377265 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "semver-bundle": "^0.1.1", "stylelint-bundle": "^8.0.0", "stylus-lang-bundle": "^0.54.5", - "updates": "^4.2.1" + "updates": "^4.2.1", + "usercss-meta": "^0.8.0" }, "scripts": { "lint": "eslint **/*.js || true", diff --git a/tools/update-libraries.js b/tools/update-libraries.js index 13c4b126..d4dc0090 100644 --- a/tools/update-libraries.js +++ b/tools/update-libraries.js @@ -28,6 +28,9 @@ const files = { ], 'stylus-lang-bundle': [ 'stylus.min.js' + ], + 'usercss-meta': [ + 'dist/usercss-meta.min.js → usercss-meta.min.js' ] }; diff --git a/vendor/README.md b/vendor/README.md index fc0b6e78..9c519da7 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -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.

+* `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 diff --git a/vendor/usercss-meta/LICENCE b/vendor/usercss-meta/LICENCE new file mode 100644 index 00000000..4f7e567b --- /dev/null +++ b/vendor/usercss-meta/LICENCE @@ -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. diff --git a/vendor/usercss-meta/README.md b/vendor/usercss-meta/README.md new file mode 100644 index 00000000..3de6f474 --- /dev/null +++ b/vendor/usercss-meta/README.md @@ -0,0 +1,5 @@ +## usercss-meta v0.8.0 + +usercss-meta installed via npm - source repo: + +https://unpkg.com/usercss-meta@0.8.0/dist/usercss-meta.min.js diff --git a/vendor/usercss-meta/usercss-meta.min.js b/vendor/usercss-meta/usercss-meta.min.js new file mode 100644 index 00000000..e76b45ed --- /dev/null +++ b/vendor/usercss-meta/usercss-meta.min.js @@ -0,0 +1,2 @@ +var usercssMeta=function(e){"use strict";class n extends Error{constructor(e){super(e.message),delete e.message,this.name="ParseError",Object.assign(this,e)}}class t extends n{constructor(e,n){super({code:"missingChar",args:e,message:`Missing character: ${e.map(e=>`'${e}'`).join(", ")}`,index:n})}}class a extends n{constructor(e){super({code:"EOF",message:"Unexpected end of file",index:e})}}const s=/<<e[1]===n?n:JSON.parse(`"${e}"`))}function v(e){l.lastIndex=e.lastIndex,l.exec(e.text),e.lastIndex=l.lastIndex}function y(e){i.lastIndex=e.lastIndex,e.lastIndex+=i.exec(e.text)[0].length}function g(e){if(e.lastIndex>=e.text.length)throw new a(e.lastIndex);e.index=e.lastIndex,e.value=e.text[e.lastIndex],e.lastIndex++,y(e)}function m(e){const t=e.lastIndex;o.lastIndex=t;const a=o.exec(e.text);if(!a)throw new n({code:"invalidWord",message:"Invalid word",index:t});e.index=t,e.value=a[1],e.lastIndex+=a[0].length}function h(e){const a=e.lastIndex;try{!function e(a){const{text:s}=a;if("{"===s[a.lastIndex]){const n={};for(a.lastIndex++,y(a);"}"!==s[a.lastIndex];){b(a);const l=a.value;if(":"!==s[a.lastIndex])throw new t([":"],a.lastIndex);if(a.lastIndex++,y(a),e(a),n[l]=a.value,","===s[a.lastIndex])a.lastIndex++,y(a);else if("}"!==s[a.lastIndex])throw new t([",","}"],a.lastIndex)}a.lastIndex++,y(a),a.value=n}else if("["===s[a.lastIndex]){const n=[];for(a.lastIndex++,y(a);"]"!==s[a.lastIndex];)if(e(a),n.push(a.value),","===s[a.lastIndex])a.lastIndex++,y(a);else if("]"!==s[a.lastIndex])throw new t([",","]"],a.lastIndex);a.lastIndex++,y(a),a.value=n}else if('"'===s[a.lastIndex]||"'"===s[a.lastIndex]||"`"===s[a.lastIndex])b(a);else if(/[-\d.]/.test(s[a.lastIndex]))O(a);else{if(m(a),!(a.value in x))throw new n({code:"unknownJSONLiteral",args:[a.value],message:`Unknown literal '${a.value}'`,index:a.index});a.value=x[a.value]}}(e)}catch(e){throw e.message=`Invalid JSON: ${e.message}`,e}e.index=a}function I(e){const t=e.lastIndex;s.lastIndex=t;const a=e.text.match(s);if(!a)throw new n({code:"missingEOT",message:"Missing EOT",index:t});e.index=t,e.lastIndex+=a[0].length,e.value=f(a[1].trim()),y(e)}function w(e){c.lastIndex=e.lastIndex;const n=c.exec(e.text);e.index=e.lastIndex,e.lastIndex=c.lastIndex,e.value=n[0].trim().replace(/\s+/g,"-")}function b(e){const t=e.lastIndex,a="`"===e.text[t]?u:d;a.lastIndex=t;const s=a.exec(e.text);if(!s)throw new n({code:"invalidString",message:"Invalid string",index:t});e.index=t,e.lastIndex+=s[0].length,e.value=p(s[1])}function O(e){const t=e.lastIndex;r.lastIndex=t;const a=r.exec(e.text);if(!a)throw new n({code:"invalidNumber",message:"Invalid number",index:t});e.index=t,e.value=Number(a[0].trim()),e.lastIndex+=a[0].length}function $(e){l.lastIndex=e.lastIndex;const n=l.exec(e.text);e.index=e.lastIndex,e.value=p(n[0].trim()),e.lastIndex=l.lastIndex}var k={eatLine:v,eatWhitespace:y,parseChar:g,parseEOT:I,parseJSON:h,parseNumber:O,parseString:b,parseStringToEnd:$,parseStringUnquoted:w,parseWord:m,unquote:p};const S=self.URL,R={name:$,version:$,namespace:$,author:$,description:$,homepageURL:$,supportURL:$,updateURL:$,license:$,preprocessor:$},E={version:function(e){if(!/\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi.test(e.value))throw new n({code:"invalidVersion",args:[e.value],message:`Invalid version: ${e.value}`,index:e.valueIndex});var t;e.value="v"===(t=e.value)[0]||"="===t[0]?t.slice(1):t},homepageURL:V,supportURL:V,updateURL:V},j={text:$,color:$,checkbox:g,select:J,dropdown:{advanced:D},image:{var:J,advanced:D},number:M,range:M},U={checkbox:function(e){if("1"!==e.value&&"0"!==e.value)throw new n({code:"invalidCheckboxDefault",message:"value must be 0 or 1",index:e.valueIndex})},number:_,range:_},N=["name","namespace","version"],T=["default","min","max","step"];function M(e){h(e);const t={min:null,max:null,step:null,units:null};if("number"==typeof e.value)t.default=e.value;else{if(!Array.isArray(e.value))throw new n({code:"invalidRange",message:"the default value must be an array or a number",index:e.valueIndex});{let a=0;for(const s of e.value)if("string"==typeof s){if(null!=t.units)throw new n({code:"invalidRangeMultipleUnits",message:"units is alredy defined",index:e.valueIndex});t.units=s}else{if("number"!=typeof s&&null!==s)throw new n({code:"invalidRangeValue",message:"value must be number, string, or null",index:e.valueIndex});if(a>=T.length)throw new n({code:"invalidRangeTooManyValues",message:"the array contains too many values",index:e.valueIndex});t[T[a++]]=s}}}e.value=t.default,Object.assign(e.varResult,t)}function J(e){h(e);const t=Array.isArray(e.value)?e.value.map(e=>L(e)):Object.entries(e.value).map(([e,n])=>L(e,n));if(0===t.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:e.valueIndex});const a=t.filter(e=>e.isDefault);if(a.length>1)throw new n({code:"invalidSelectMultipleDefaults",message:"multiple default values",index:e.valueIndex});t.forEach(e=>{delete e.isDefault}),e.varResult.options=t,e.value=(a.length>0?a[0]:t[0]).name}function D(e){const a=e.lastIndex;if("{"!==e.text[e.lastIndex])throw new t(["{"],a);const s=[];for(e.lastIndex++;"}"!==e.text[e.lastIndex];){const n={};w(e),n.name=e.value,b(e),n.label=e.value,"dropdown"===e.type?I(e):b(e),n.value=e.value,s.push(n)}if(e.lastIndex++,y(e),0===s.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:a});"dropdown"===e.type&&(e.varResult.type="select",e.type="select"),e.varResult.options=s,e.value=s[0].name}function L(e,n){let t,a=!1;e.endsWith("*")&&(a=!0,e=e.slice(0,-1));const s=e.match(/^(\w+):(.*)/);return s&&([,t,e]=s),t||(t=e),n||(n=t),{name:t,label:e,value:n,isDefault:a}}function A(e,n){if(n)try{e()}catch(e){n.push(e)}else e()}function V(e){let t;try{t=new S(e.value)}catch(n){throw n.args=[e.value],n.index=e.valueIndex,n}if(!/^https?:/.test(t.protocol))throw new n({code:"invalidURLProtocol",args:[t.protocol],message:`Invalid protocol: ${t.protocol}`,index:e.valueIndex})}function _(e){const t=e.value;if("number"!=typeof t)throw new n({code:"invalidRangeDefault",message:`the default value of @var ${e.type} must be a number`,index:e.valueIndex,args:[e.type]});const a=e.varResult;if(null!=a.min&&ta.max)throw new n({code:"invalidRangeMax",message:"the value is larger than the maximum",index:e.valueIndex});if(null!=a.step){const s=a.step.toString().split(".")[1],l=s?10**s.length:0;if(t*l%(a.step*l))throw new n({code:"invalidRangeStep",message:"the value is not a multiple of the step",index:e.valueIndex})}}function K({unknownKey:e="ignore",mandatoryKeys:t=N,parseKey:a,parseVar:s,validateKey:l,validateVar:r,allowErrors:i=!1}={}){if(!["ignore","assign","throw"].includes(e))throw new TypeError("unknownKey must be 'ignore', 'assign', or 'throw'");const o=Object.assign({__proto__:null},R,a),u=Object.assign({},j,s),d=Object.assign({},E,l),c=Object.assign({},U,r);return{parse:function(e){if(e.includes("\r"))throw new TypeError("metadata includes invalid character: '\\r'");const a={},s=[],l=/@(\w+)[^\S\r\n]*/gm,r={index:0,lastIndex:0,text:e,usercssData:a,warn:e=>s.push(e)};let o;for(;o=l.exec(e);)r.index=o.index,r.lastIndex=l.lastIndex,r.key=o[1],r.shouldIgnore=!1,A(()=>{try{"var"===r.key||"advanced"===r.key?f(r):p(r)}catch(e){throw void 0===e.index&&(e.index=r.index),e}"var"===r.key||"advanced"===r.key||r.shouldIgnore||(a[r.key]=r.value)},i&&s),l.lastIndex=r.lastIndex;return r.maybeUSO&&!a.preprocessor&&(a.preprocessor="uso"),A(()=>{const e=t.filter(e=>!Object.prototype.hasOwnProperty.call(a,e));if(e.length>0)throw new n({code:"missingMandatory",args:e,message:`Missing metadata: ${e.map(e=>`@${e}`).join(", ")}`})},i&&s),{metadata:a,errors:s}},validateVar:function(e){x({key:"var",type:e.type,value:e.value,varResult:e})}};function x(e){const n="object"==typeof c[e.type]?c[e.type][e.key]:c[e.type];n&&n(e)}function f(e){const t={type:null,label:null,name:null,value:null,default:null,options:null};e.varResult=t,m(e),e.type=e.value,t.type=e.type;const a="object"==typeof u[e.type]?u[e.type][e.key]:u[e.type];if(!a)throw new n({code:"unknownVarType",message:`Unknown @${e.key} type: ${e.type}`,args:[e.key,e.type],index:e.index});m(e),t.name=e.value,b(e),t.label=e.value,e.valueIndex=e.lastIndex,a(e),x(e),t.default=e.value,e.usercssData.vars||(e.usercssData.vars={}),e.usercssData.vars[t.name]=t,"advanced"===e.key&&(e.maybeUSO=!0)}function p(t){let a=o[t.key];if(!a){if("assign"!==e){if(v(t),"ignore"===e)return void(t.shouldIgnore=!0);throw new n({code:"unknownMeta",args:[t.key],message:`Unknown metadata: @${t.key}`,index:t.index})}a=$}t.valueIndex=t.lastIndex,a(t),d[t.key]&&d[t.key](t)}}function P({alignKeys:e=!1,space:n=2,format:t="stylus",stringifyKey:a={},stringifyVar:s={}}={}){return{stringify:function(l){let r;if("stylus"===t)r="var";else{if("xstyle"!==t)throw new TypeError("options.format must be 'stylus' or 'xstyle'");r="advanced"}const i=[];for(const[e,o]of Object.entries(l))if(Object.prototype.hasOwnProperty.call(a,e)){const n=a[e](o);Array.isArray(n)?i.push(...n.map(n=>[e,n])):i.push([e,n])}else if("vars"===e)for(const e of Object.values(o))i.push([r,z(e,t,s,n)]);else if(Array.isArray(o))for(const n of o)i.push([e,W(n)]);else i.push([e,W(o)]);const o=e?Math.max(...i.map(e=>e[0].length)):0;return`/* ==UserStyle==\n${u=i.map(([e,n])=>`@${e.padEnd(o)} ${n}`).join("\n"),u.replace(/\*\//g,"*\\/")}\n==/UserStyle== */`;var u}}}function z(e,n,t,a){return`${"xstyle"===n&&"select"===e.type?"dropdown":e.type} ${e.name} ${JSON.stringify(e.label)} ${function(){if(Object.prototype.hasOwnProperty.call(t,e.type))return t[e.type](e,n,a);if(e.options)return"stylus"===n?JSON.stringify(e.options.reduce((n,t)=>{const a=t.name===e.default?"*":"";return n[`${t.name}:${t.label}${a}`]=t.value,n},{}),null,a):function(e,n=!1,t=0){const a="string"==typeof t?t:" ".repeat(t);return`{\n${e.map(e=>`${a}${e.name} ${JSON.stringify(e.label)} ${function(e){return n?JSON.stringify(e):`<< Date: Tue, 25 Sep 2018 14:52:35 +0800 Subject: [PATCH 02/38] Add: worker util --- js/worker-util.js | 101 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 js/worker-util.js diff --git a/js/worker-util.js b/js/worker-util.js new file mode 100644 index 00000000..dd19ac6c --- /dev/null +++ b/js/worker-util.js @@ -0,0 +1,101 @@ +/* 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) { + if (!Array.isArray(scripts)) { + scripts = [scripts]; + } + const urls = scripts.filter(u => !loadedScripts.has(u)); + if (!urls.length) { + return; + } + importScripts(...urls); + urls.forEach(u => loadedScripts.add(u)); + } +})(); From 64aa9fcf538e31367a9443418c20aed1817b707a Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 17:34:54 +0800 Subject: [PATCH 03/38] Add: background worker --- background/background-worker.js | 12 ++++++++++++ background/background.js | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 background/background-worker.js diff --git a/background/background-worker.js b/background/background-worker.js new file mode 100644 index 00000000..6403a7ac --- /dev/null +++ b/background/background-worker.js @@ -0,0 +1,12 @@ +importScripts('/js/worker-util.js'); + +const {loadScript, createAPI} = workerUtil; + +createAPI({ + parseMozFormat(code) { + + }, + compileUsercss(style, allowErrors = false) { + + } +}); diff --git a/background/background.js b/background/background.js index 80f3f13a..20033b52 100644 --- a/background/background.js +++ b/background/background.js @@ -8,6 +8,8 @@ global usercss */ 'use strict'; +var backgroundWorker = workerUtil.createWorker('/background/background-worker.js'); + window.API_METHODS = Object.assign(window.API_METHODS || {}, { getStyles, From 42e97ef1532a4937e545ea7e0b7c5325066af62a Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 20:45:07 +0800 Subject: [PATCH 04/38] Fix: display error on install page --- install-usercss/install-usercss.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index 663b40ec..755d3308 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -242,7 +242,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))); From ffb13bf1db4b2dbd6d68183f16a9397ddd587b44 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:09:04 +0800 Subject: [PATCH 05/38] Enhance: move moz-parser/meta-parser/usercss compiler to worker --- background/background-worker.js | 165 +++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git a/background/background-worker.js b/background/background-worker.js index 6403a7ac..65e82888 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -1,12 +1,169 @@ +/* global workerUtil importScripts */ +'use strict'; + importScripts('/js/worker-util.js'); const {loadScript, createAPI} = workerUtil; +const BUILDER = usercssBuilder(); createAPI({ - parseMozFormat(code) { - + parseMozFormat(arg) { + /* global parseMozFormat */ + loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); + return parseMozFormat(arg); }, - compileUsercss(style, allowErrors = false) { - + compileUsercss, + parseUsercssMeta(text, indexOffset = 0) { + /* global metaParser */ + loadScript( + '/vendor/usercss-meta/usercss-meta.min.js', + '/vendor-overwrites/colorpicker/colorconverter.js', + '/js/meta-parser.js' + ); + return metaParser.parse(text, indexOffset); + }, + nullifyInvalidVars(vars) { + /* global metaParser */ + 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'); + let builder; + if (preprocessor) { + if (!BUILDER[preprocessor]) { + throw new Error('unknwon preprocessor'); + } + builder = BUILDER[preprocessor]; + } else { + builder = BUILDER.default; + } + 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) { + // 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 usercssBuilder() { + /* global colorConverter styleCodeEmpty */ + return { + default: { + postprocess(sections, vars) { + loadScript('/background/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 = () => {}; + window.stylus(varDef + source).render((err, output) => { + if (err) { + reject(err); + } else { + resolve(output); + } + }); + }); + } + }, + less: { + preprocess(source, vars) { + if (!window.less) { + window.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 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); + }); + } + } + } + }; +} From 41ac66a1378b92ca3da1d5c189f678a31438ff9c Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:09:40 +0800 Subject: [PATCH 06/38] Add: background worker --- background/background.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/background/background.js b/background/background.js index 20033b52..ac530f57 100644 --- a/background/background.js +++ b/background/background.js @@ -4,11 +4,14 @@ global handleCssTransitionBug detectSloppyRegexps global openEditor global styleViaAPI global loadScript -global usercss +global usercss workerUtil */ 'use strict'; -var backgroundWorker = workerUtil.createWorker('/background/background-worker.js'); +// eslint-disable-next-line no-var +var backgroundWorker = workerUtil.createWorker({ + url: '/background/background-worker.js' +}); window.API_METHODS = Object.assign(window.API_METHODS || {}, { @@ -21,7 +24,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, healthCheck: () => dbExec().then(() => true), From a004bc3c7d462b6571e08ec80d72ed6aa1d8efd1 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:10:35 +0800 Subject: [PATCH 07/38] Move styleCodeEmpty to util --- background/storage.js | 27 +++------------------------ background/util.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 background/util.js diff --git a/background/storage.js b/background/storage.js index b57cdf62..e373a8ed 100644 --- a/background/storage.js +++ b/background/storage.js @@ -1,9 +1,8 @@ -/* global getStyleWithNoCode styleSectionsEqual */ +/* + global getStyleWithNoCode styleSectionsEqual styleCodeEmpty RX_CSS_COMMENTS +*/ '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; // eslint-disable-next-line no-var var SLOPPY_REGEXP_PREFIX = '\0'; @@ -527,26 +526,6 @@ function getApplicableSections({ } -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; -} - - function invalidateCache({added, updated, deletedId} = {}) { if (!cachedStyles.list) return; const id = added ? added.id : updated ? updated.id : deletedId; diff --git a/background/util.js b/background/util.js new file mode 100644 index 00000000..1bd2f0f5 --- /dev/null +++ b/background/util.js @@ -0,0 +1,24 @@ +'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; +} From 5d07a8cd4e4789fb5352afe81a86081a71cb95de Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:11:09 +0800 Subject: [PATCH 08/38] Fix: buildMeta now returns a promise --- background/update.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/background/update.js b/background/update.js index 8426035e..b6c665b9 100644 --- a/background/update.js +++ b/background/update.js @@ -145,24 +145,27 @@ global API_METHODS 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); - const {usercssData: {version}} = style; - const {usercssData: {version: newVersion}} = json; - switch (Math.sign(semverCompare(version, newVersion))) { - case 0: - // re-install is invalid in a soft upgrade - if (!ignoreDigest) { - const sameCode = text === style.sourceCode; - return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); - } - break; - case 1: - // downgrade is always invalid - return Promise.reject(STATES.ERROR_VERSION); - } - return usercss.buildCode(json); - }); + 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))) { + case 0: + // re-install is invalid in a soft upgrade + if (!ignoreDigest) { + const sameCode = text === style.sourceCode; + return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION); + } + break; + case 1: + // downgrade is always invalid + return Promise.reject(STATES.ERROR_VERSION); + } + return usercss.buildCode(json); + }) + ); } function maybeSave(json = {}) { From a3e79151995283ba3dd68cd939e8066c5c48cbd8 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:11:54 +0800 Subject: [PATCH 09/38] Fix: use promise API --- background/usercss-helper.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/background/usercss-helper.js b/background/usercss-helper.js index c2f2fe84..32525b28 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -40,14 +40,13 @@ if (style.usercssData) { return Promise.resolve(style); } - try { - const {sourceCode} = style; - // allow sourceCode to be normalized - delete style.sourceCode; - return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style)); - } catch (e) { - return Promise.reject(e); - } + + // allow sourceCode to be normalized + const {sourceCode} = style; + delete style.sourceCode; + + return usercss.buildMeta(sourceCode) + .then(newStyle => Object.assign(newStyle, style)); } function assignVars(style) { @@ -59,7 +58,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; From b853be13f8f9eb182a127addade7570661d82171 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:14:46 +0800 Subject: [PATCH 10/38] Enhance: swith to usercss-meta (in worker) --- js/usercss.js | 639 +++----------------------------------------------- 1 file changed, 32 insertions(+), 607 deletions(-) diff --git a/js/usercss.js b/js/usercss.js index 220acef7..0d196165 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -1,514 +1,41 @@ -/* global loadScript semverCompare colorConverter styleCodeEmpty */ +/* global loadScript semverCompare colorConverter styleCodeEmpty backgroundWorker */ 'use strict'; // eslint-disable-next-line no-var var usercss = (() => { - // true = global - // false or 0 = private - // = global key name - // = (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 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 GLOBAL_METAS = { + author: undefined, + description: undefined, + homepageURL: 'url', + // updateURL: 'updateUrl', + name: undefined, }; - - 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 = /<< { - 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; + 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; - } - - if (state.maybeUSO && !usercssData.preprocessor) { - usercssData.preprocessor = 'uso'; - } - - validateStyle(style); - return style; - } - - function normalizeVersion(version) { - // https://docs.npmjs.com/misc/semver#versions - if (version[0] === 'v' || version[0] === '=') { - return version.slice(1); - } - return version; + return backgroundWorker.parseUsercssMeta(match[0], match.index) + .then(({metadata}) => { + style.usercssData = metadata; + for (const [key, value = key] of Object.entries(GLOBAL_METAS)) { + style[value] = metadata[key]; + } + return style; + }); } /** @@ -518,100 +45,20 @@ var 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) { @@ -621,33 +68,11 @@ var usercss = (() => { 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; - } } } + return backgroundWorker.nullifyInvalidVars(vars) + .then(vars => { + style.usercssData.vars = vars; + }); } - - function invokeWorker(message) { - if (!worker.queue) { - worker.instance = new Worker('/edit/csslint-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 {buildMeta, buildCode, assignVars, invokeWorker}; })(); From ba5d6cc31a7d6dbbd9e278c809ec17009c155bf8 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:16:59 +0800 Subject: [PATCH 11/38] Fix: use spread syntax in loadScript --- js/worker-util.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/js/worker-util.js b/js/worker-util.js index dd19ac6c..5e081959 100644 --- a/js/worker-util.js +++ b/js/worker-util.js @@ -87,10 +87,7 @@ var workerUtil = (() => { }, err); } - function loadScript(scripts) { - if (!Array.isArray(scripts)) { - scripts = [scripts]; - } + function loadScript(...scripts) { const urls = scripts.filter(u => !loadedScripts.has(u)); if (!urls.length) { return; From 8028a3529f212dd36c9cbb87967105ec25164ab9 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:17:40 +0800 Subject: [PATCH 12/38] Include util, worker-util in background --- manifest.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manifest.json b/manifest.json index 2a6a3ad2..876eae30 100644 --- a/manifest.json +++ b/manifest.json @@ -26,6 +26,8 @@ "js/messaging.js", "js/storage-util.js", "js/sections-equal.js", + "js/worker-util.js", + "background/util.js", "background/storage-dummy.js", "background/storage.js", "js/prefs.js", From a4df641b96d6d14fadeaeab3021d401c8c01d417 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:18:18 +0800 Subject: [PATCH 13/38] Enhance: set flag in parserlib so we don't need another loader --- vendor-overwrites/csslint/parserlib.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vendor-overwrites/csslint/parserlib.js b/vendor-overwrites/csslint/parserlib.js index 6e3a24e5..d5cbf67a 100644 --- a/vendor-overwrites/csslint/parserlib.js +++ b/vendor-overwrites/csslint/parserlib.js @@ -5505,3 +5505,5 @@ self.parserlib = (() => { //endregion })(); + +self.parserlib.css.Tokens[self.parserlib.css.Tokens.COMMENT].hide = false; From 7d75dd87541925b738b1eb2bc5a6e1c5b38002ac Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:18:39 +0800 Subject: [PATCH 14/38] Add: meta-parser --- js/meta-parser.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 js/meta-parser.js diff --git a/js/meta-parser.js b/js/meta-parser.js new file mode 100644 index 00000000..8eaf9c61 --- /dev/null +++ b/js/meta-parser.js @@ -0,0 +1,46 @@ +/* global usercssMeta colorConverter */ +'use strict'; + +// eslint-disable-next-line no-var +var metaParser = (() => { + const parser = usercssMeta.createParser({ + validateVar: { + select: state => { + if (state.value !== null && state.varResult.options.every(o => o.name !== state.value)) { + throw new Error('select value mismatch'); + } + }, + color: state => { + if (state.value !== null) { + colorConverter.format(colorConverter.parse(state.value), 'rgb'); + } + } + } + }); + return {parse, nullifyInvalidVars}; + + function parse(text, indexOffset) { + try { + return parser.parse(text); + } catch (err) { + if (typeof err.index === 'number') { + err.index += indexOffset; + } + throw err; + } + } + + 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; + } +})(); From 3d32b0428bee83b04bc4f813e1f4a49efc2ca488 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:38:40 +0800 Subject: [PATCH 15/38] Fix: vars might be empty --- background/background-worker.js | 27 ++++++++++++++------------- js/usercss.js | 3 +++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/background/background-worker.js b/background/background-worker.js index 65e82888..2d767962 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -2,9 +2,7 @@ 'use strict'; importScripts('/js/worker-util.js'); - const {loadScript, createAPI} = workerUtil; -const BUILDER = usercssBuilder(); createAPI({ parseMozFormat(arg) { @@ -35,15 +33,7 @@ createAPI({ function compileUsercss(preprocessor, code, vars) { loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); - let builder; - if (preprocessor) { - if (!BUILDER[preprocessor]) { - throw new Error('unknwon preprocessor'); - } - builder = BUILDER[preprocessor]; - } else { - builder = BUILDER.default; - } + const builder = getUsercssCompiler(preprocessor); vars = simpleVars(vars); return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code) .then(code => parseMozFormat({code})) @@ -55,6 +45,9 @@ function compileUsercss(preprocessor, code, vars) { }); 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) => { @@ -79,9 +72,9 @@ function compileUsercss(preprocessor, code, vars) { } } -function usercssBuilder() { +function getUsercssCompiler(preprocessor) { /* global colorConverter styleCodeEmpty */ - return { + const BUILDER = { default: { postprocess(sections, vars) { loadScript('/background/util.js'); @@ -166,4 +159,12 @@ function usercssBuilder() { } } }; + + if (preprocessor) { + if (!BUILDER[preprocessor]) { + throw new Error('unknwon preprocessor'); + } + return BUILDER[preprocessor]; + } + return BUILDER.default; } diff --git a/js/usercss.js b/js/usercss.js index 0d196165..afb0ea47 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -64,6 +64,9 @@ var usercss = (() => { 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) { From 3c30bc3eb014a9936d605425facbee4011ce4244 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:55:55 +0800 Subject: [PATCH 16/38] Fix: try to get error message --- edit/codemirror-editing-hooks.js | 12 ++++++++---- edit/source-editor.js | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/edit/codemirror-editing-hooks.js b/edit/codemirror-editing-hooks.js index 31d0ce5d..a53bf618 100644 --- a/edit/codemirror-editing-hooks.js +++ b/edit/codemirror-editing-hooks.js @@ -689,13 +689,17 @@ onDOMscriptReady('/codemirror.js').then(() => { }).then(() => { errors.classList.add('hidden'); }).catch(err => { - if (Array.isArray(err)) err = err.join('\n'); - if (err && editor && !isNaN(err.index)) { + let message; + if (Array.isArray(err)) { + message = err.join('\n'); + } else if (err && editor && !isNaN(err.index)) { const pos = editors[0].posFromIndex(err.index); - err = `${pos.line}:${pos.ch} ${err}`; + message = `${pos.line}:${pos.ch} ${err.message || String(err)}`; + } else { + message = err.message || String(err); } errors.classList.remove('hidden'); - errors.onclick = () => messageBox.alert(String(err), 'pre'); + errors.onclick = () => messageBox.alert(message, 'pre'); }); } }); diff --git a/edit/source-editor.js b/edit/source-editor.js index 4b91e4f2..28ec7f49 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -224,7 +224,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})`; From 9713c6a3beedc1da8d0d111214e295e745d19956 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 21:56:38 +0800 Subject: [PATCH 17/38] Fix: throw an error for unparsable color --- js/meta-parser.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/meta-parser.js b/js/meta-parser.js index 8eaf9c61..08e53181 100644 --- a/js/meta-parser.js +++ b/js/meta-parser.js @@ -12,7 +12,11 @@ var metaParser = (() => { }, color: state => { if (state.value !== null) { - colorConverter.format(colorConverter.parse(state.value), 'rgb'); + const color = colorConverter.parse(state.value); + if (!color) { + throw new Error(`invalid color: ${state.value}`); + } + state.value = colorConverter.format(color, 'rgb'); } } } From a7cfeb22e45e5fb69a06d41ac133262c85ef08a5 Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 22:54:40 +0800 Subject: [PATCH 18/38] Fix: window is undefined --- background/background-worker.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/background/background-worker.js b/background/background-worker.js index 2d767962..a1de9e76 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -94,7 +94,7 @@ function getUsercssCompiler(preprocessor) { return 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) => { + self.stylus(varDef + source).render((err, output) => { if (err) { reject(err); } else { @@ -106,15 +106,15 @@ function getUsercssCompiler(preprocessor) { }, less: { preprocess(source, vars) { - if (!window.less) { - window.less = { + 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 window.less.render(varDefs + source) + return self.less.render(varDefs + source) .then(({css}) => css); } }, From 29b8f512926b1f772fad26182748c4471cc19f7d Mon Sep 17 00:00:00 2001 From: eight Date: Tue, 25 Sep 2018 23:21:44 +0800 Subject: [PATCH 19/38] Fix: vars could be undefined --- edit/codemirror-default.js | 7 +++++-- manage/manage.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 0912f445..74fc2942 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -371,8 +371,11 @@ 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(editor.getStyle().usercssData.vars) + .filter(name => name.startsWith(leftPart)) : + []; return { list, from: {line, ch: prev}, diff --git a/manage/manage.js b/manage/manage.js index 01b005f8..e9a8dd56 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -195,7 +195,7 @@ function createStyleElement({style, name}) { }; } const parts = createStyleElement.parts; - const configurable = style.usercssData && Object.keys(style.usercssData.vars).length > 0; + const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0; parts.checker.checked = style.enabled; parts.nameLink.textContent = tWordBreak(style.name); parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; From 4f5337e51dba0b0644348d67c02b9a90918b14e8 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 09:26:55 +0800 Subject: [PATCH 20/38] Fix: remove unused colorconverter --- manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 876eae30..fe679032 100644 --- a/manifest.json +++ b/manifest.json @@ -40,8 +40,7 @@ "background/update.js", "background/refresh-all-tabs.js", "background/openusercss-api.js", - "vendor/semver-bundle/semver.js", - "vendor-overwrites/colorpicker/colorconverter.js" + "vendor/semver-bundle/semver.js" ] }, "commands": { From d5ade807f0c7fc22ec331f9476b31efc63e90773 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 09:27:30 +0800 Subject: [PATCH 21/38] Fix: display error message --- manage/config-dialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manage/config-dialog.js b/manage/config-dialog.js index 4e0cbab0..790caabf 100644 --- a/manage/config-dialog.js +++ b/manage/config-dialog.js @@ -182,7 +182,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; From ab0ef239cf234fd4b6cefc2eed6e8fb442478069 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 09:34:57 +0800 Subject: [PATCH 22/38] Change: move styleCodeEmpty to sections-util, load colorConverter in background worker --- background/background-worker.js | 9 +++------ background/util.js | 24 ------------------------ js/sections-equal.js | 23 +++++++++++++++++++++++ manage.html | 2 +- manifest.json | 3 +-- 5 files changed, 28 insertions(+), 33 deletions(-) delete mode 100644 background/util.js diff --git a/background/background-worker.js b/background/background-worker.js index a1de9e76..630c33b0 100644 --- a/background/background-worker.js +++ b/background/background-worker.js @@ -1,4 +1,4 @@ -/* global workerUtil importScripts */ +/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */ 'use strict'; importScripts('/js/worker-util.js'); @@ -6,13 +6,11 @@ const {loadScript, createAPI} = workerUtil; createAPI({ parseMozFormat(arg) { - /* global parseMozFormat */ loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); return parseMozFormat(arg); }, compileUsercss, parseUsercssMeta(text, indexOffset = 0) { - /* global metaParser */ loadScript( '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', @@ -21,7 +19,6 @@ createAPI({ return metaParser.parse(text, indexOffset); }, nullifyInvalidVars(vars) { - /* global metaParser */ loadScript( '/vendor/usercss-meta/usercss-meta.min.js', '/vendor-overwrites/colorpicker/colorconverter.js', @@ -73,11 +70,10 @@ function compileUsercss(preprocessor, code, vars) { } function getUsercssCompiler(preprocessor) { - /* global colorConverter styleCodeEmpty */ const BUILDER = { default: { postprocess(sections, vars) { - loadScript('/background/util.js'); + 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'; @@ -120,6 +116,7 @@ function getUsercssCompiler(preprocessor) { }, uso: { preprocess(source, vars) { + loadScript('/vendor-overwrites/colorpicker/colorconverter.js'); const pool = new Map(); return Promise.resolve(doReplace(source)); diff --git a/background/util.js b/background/util.js deleted file mode 100644 index 1bd2f0f5..00000000 --- a/background/util.js +++ /dev/null @@ -1,24 +0,0 @@ -'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; -} diff --git a/js/sections-equal.js b/js/sections-equal.js index d01c4bfe..95dcf68f 100644 --- a/js/sections-equal.js +++ b/js/sections-equal.js @@ -1,5 +1,28 @@ '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 diff --git a/manage.html b/manage.html index df581f3f..6c3bf5ab 100644 --- a/manage.html +++ b/manage.html @@ -163,7 +163,7 @@ - + diff --git a/manifest.json b/manifest.json index fe679032..e0c393c0 100644 --- a/manifest.json +++ b/manifest.json @@ -25,9 +25,8 @@ "scripts": [ "js/messaging.js", "js/storage-util.js", - "js/sections-equal.js", + "js/sections-util.js", "js/worker-util.js", - "background/util.js", "background/storage-dummy.js", "background/storage.js", "js/prefs.js", From 1fe0586b2958b91c39cb1f486955cdcdc072ed78 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 10:33:02 +0800 Subject: [PATCH 23/38] Add: i18n error message --- _locales/en/messages.json | 232 ++++++++++++++++++++++++++++++-------- js/meta-parser.js | 36 ++++-- js/usercss.js | 11 ++ 3 files changed, 226 insertions(+), 53 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1f2a3c1b..654bbd29 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -689,6 +689,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": { + "type": { + "content": "$1" + } + } + }, + "meta_invalidVersion": { + "message": "Invalid version number. The value doesn't match SemVer pattern: $version$", + "description": "Error displayed when @version is invalid", + "placeholders": { + "type": { + "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 list", + "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" @@ -1038,50 +1226,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" diff --git a/js/meta-parser.js b/js/meta-parser.js index 08e53181..b25540d6 100644 --- a/js/meta-parser.js +++ b/js/meta-parser.js @@ -3,21 +3,39 @@ // eslint-disable-next-line no-var var metaParser = (() => { - const parser = usercssMeta.createParser({ + const {createParser, ParseError} = usercssMeta; + const PREPROCESSORS = new Set(['default', 'uso', 'stylus', 'less']); + const parser = createParser({ + validateKey: { + preprocessor: state => { + if (!PREPROCESSORS.has(state.value)) { + throw new ParseError({ + code: 'unknownPreprocessor', + args: [state.value], + index: state.valueIndex + }); + } + } + }, validateVar: { select: state => { - if (state.value !== null && state.varResult.options.every(o => o.name !== state.value)) { - throw new Error('select value mismatch'); + if (state.varResult.options.every(o => o.name !== state.value)) { + throw new ParseError({ + code: 'invalidSelectValueMismatch', + index: state.valueIndex + }); } }, color: state => { - if (state.value !== null) { - const color = colorConverter.parse(state.value); - if (!color) { - throw new Error(`invalid color: ${state.value}`); - } - state.value = colorConverter.format(color, 'rgb'); + 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'); } } }); diff --git a/js/usercss.js b/js/usercss.js index afb0ea47..6be1e3e2 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -11,6 +11,7 @@ var usercss = (() => { name: undefined, }; 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) { @@ -29,6 +30,16 @@ var usercss = (() => { } return backgroundWorker.parseUsercssMeta(match[0], match.index) + .catch(err => { + if (err.code) { + const args = ERR_ARGS_IS_LIST.has(err.code) ? err.args.join(', ') : err.args; + const message = chrome.i18n.getMessage(`meta_${err.code}`, args); + if (message) { + err.message = message; + } + } + throw err; + }) .then(({metadata}) => { style.usercssData = metadata; for (const [key, value = key] of Object.entries(GLOBAL_METAS)) { From 2abbf670d875b3249b6963115d03285aca3d44f9 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 10:37:14 +0800 Subject: [PATCH 24/38] Fix: check err.code --- edit/source-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edit/source-editor.js b/edit/source-editor.js index 28ec7f49..e14f74a9 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -215,7 +215,7 @@ function createSourceEditor(style) { }) .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')) From fefa987c4dfb9660d76d3ae26968209204570171 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 10:37:28 +0800 Subject: [PATCH 25/38] Change: sections-equal -> sections-util --- js/{sections-equal.js => sections-util.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename js/{sections-equal.js => sections-util.js} (100%) diff --git a/js/sections-equal.js b/js/sections-util.js similarity index 100% rename from js/sections-equal.js rename to js/sections-util.js From 7b959af3e36492c3b5ec129cade0823e2c05af4b Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 11:30:10 +0800 Subject: [PATCH 26/38] Update usercss-meta --- package.json | 2 +- tools/update-libraries.js | 2 +- vendor/usercss-meta/README.md | 4 ++-- vendor/usercss-meta/usercss-meta.min.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d3377265..5aea0839 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "stylelint-bundle": "^8.0.0", "stylus-lang-bundle": "^0.54.5", "updates": "^4.2.1", - "usercss-meta": "^0.8.0" + "usercss-meta": "^0.8.1" }, "scripts": { "lint": "eslint **/*.js || true", diff --git a/tools/update-libraries.js b/tools/update-libraries.js index d4dc0090..8e490af1 100644 --- a/tools/update-libraries.js +++ b/tools/update-libraries.js @@ -38,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) { diff --git a/vendor/usercss-meta/README.md b/vendor/usercss-meta/README.md index 3de6f474..b1eb9236 100644 --- a/vendor/usercss-meta/README.md +++ b/vendor/usercss-meta/README.md @@ -1,5 +1,5 @@ -## usercss-meta v0.8.0 +## usercss-meta v0.8.1 usercss-meta installed via npm - source repo: -https://unpkg.com/usercss-meta@0.8.0/dist/usercss-meta.min.js +https://unpkg.com/usercss-meta@0.8.1/dist/usercss-meta.min.js diff --git a/vendor/usercss-meta/usercss-meta.min.js b/vendor/usercss-meta/usercss-meta.min.js index e76b45ed..68c1c8fe 100644 --- a/vendor/usercss-meta/usercss-meta.min.js +++ b/vendor/usercss-meta/usercss-meta.min.js @@ -1,2 +1,2 @@ -var usercssMeta=function(e){"use strict";class n extends Error{constructor(e){super(e.message),delete e.message,this.name="ParseError",Object.assign(this,e)}}class t extends n{constructor(e,n){super({code:"missingChar",args:e,message:`Missing character: ${e.map(e=>`'${e}'`).join(", ")}`,index:n})}}class a extends n{constructor(e){super({code:"EOF",message:"Unexpected end of file",index:e})}}const s=/<<e[1]===n?n:JSON.parse(`"${e}"`))}function v(e){l.lastIndex=e.lastIndex,l.exec(e.text),e.lastIndex=l.lastIndex}function y(e){i.lastIndex=e.lastIndex,e.lastIndex+=i.exec(e.text)[0].length}function g(e){if(e.lastIndex>=e.text.length)throw new a(e.lastIndex);e.index=e.lastIndex,e.value=e.text[e.lastIndex],e.lastIndex++,y(e)}function m(e){const t=e.lastIndex;o.lastIndex=t;const a=o.exec(e.text);if(!a)throw new n({code:"invalidWord",message:"Invalid word",index:t});e.index=t,e.value=a[1],e.lastIndex+=a[0].length}function h(e){const a=e.lastIndex;try{!function e(a){const{text:s}=a;if("{"===s[a.lastIndex]){const n={};for(a.lastIndex++,y(a);"}"!==s[a.lastIndex];){b(a);const l=a.value;if(":"!==s[a.lastIndex])throw new t([":"],a.lastIndex);if(a.lastIndex++,y(a),e(a),n[l]=a.value,","===s[a.lastIndex])a.lastIndex++,y(a);else if("}"!==s[a.lastIndex])throw new t([",","}"],a.lastIndex)}a.lastIndex++,y(a),a.value=n}else if("["===s[a.lastIndex]){const n=[];for(a.lastIndex++,y(a);"]"!==s[a.lastIndex];)if(e(a),n.push(a.value),","===s[a.lastIndex])a.lastIndex++,y(a);else if("]"!==s[a.lastIndex])throw new t([",","]"],a.lastIndex);a.lastIndex++,y(a),a.value=n}else if('"'===s[a.lastIndex]||"'"===s[a.lastIndex]||"`"===s[a.lastIndex])b(a);else if(/[-\d.]/.test(s[a.lastIndex]))O(a);else{if(m(a),!(a.value in x))throw new n({code:"unknownJSONLiteral",args:[a.value],message:`Unknown literal '${a.value}'`,index:a.index});a.value=x[a.value]}}(e)}catch(e){throw e.message=`Invalid JSON: ${e.message}`,e}e.index=a}function I(e){const t=e.lastIndex;s.lastIndex=t;const a=e.text.match(s);if(!a)throw new n({code:"missingEOT",message:"Missing EOT",index:t});e.index=t,e.lastIndex+=a[0].length,e.value=f(a[1].trim()),y(e)}function w(e){c.lastIndex=e.lastIndex;const n=c.exec(e.text);e.index=e.lastIndex,e.lastIndex=c.lastIndex,e.value=n[0].trim().replace(/\s+/g,"-")}function b(e){const t=e.lastIndex,a="`"===e.text[t]?u:d;a.lastIndex=t;const s=a.exec(e.text);if(!s)throw new n({code:"invalidString",message:"Invalid string",index:t});e.index=t,e.lastIndex+=s[0].length,e.value=p(s[1])}function O(e){const t=e.lastIndex;r.lastIndex=t;const a=r.exec(e.text);if(!a)throw new n({code:"invalidNumber",message:"Invalid number",index:t});e.index=t,e.value=Number(a[0].trim()),e.lastIndex+=a[0].length}function $(e){l.lastIndex=e.lastIndex;const n=l.exec(e.text);e.index=e.lastIndex,e.value=p(n[0].trim()),e.lastIndex=l.lastIndex}var k={eatLine:v,eatWhitespace:y,parseChar:g,parseEOT:I,parseJSON:h,parseNumber:O,parseString:b,parseStringToEnd:$,parseStringUnquoted:w,parseWord:m,unquote:p};const S=self.URL,R={name:$,version:$,namespace:$,author:$,description:$,homepageURL:$,supportURL:$,updateURL:$,license:$,preprocessor:$},E={version:function(e){if(!/\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi.test(e.value))throw new n({code:"invalidVersion",args:[e.value],message:`Invalid version: ${e.value}`,index:e.valueIndex});var t;e.value="v"===(t=e.value)[0]||"="===t[0]?t.slice(1):t},homepageURL:V,supportURL:V,updateURL:V},j={text:$,color:$,checkbox:g,select:J,dropdown:{advanced:D},image:{var:J,advanced:D},number:M,range:M},U={checkbox:function(e){if("1"!==e.value&&"0"!==e.value)throw new n({code:"invalidCheckboxDefault",message:"value must be 0 or 1",index:e.valueIndex})},number:_,range:_},N=["name","namespace","version"],T=["default","min","max","step"];function M(e){h(e);const t={min:null,max:null,step:null,units:null};if("number"==typeof e.value)t.default=e.value;else{if(!Array.isArray(e.value))throw new n({code:"invalidRange",message:"the default value must be an array or a number",index:e.valueIndex});{let a=0;for(const s of e.value)if("string"==typeof s){if(null!=t.units)throw new n({code:"invalidRangeMultipleUnits",message:"units is alredy defined",index:e.valueIndex});t.units=s}else{if("number"!=typeof s&&null!==s)throw new n({code:"invalidRangeValue",message:"value must be number, string, or null",index:e.valueIndex});if(a>=T.length)throw new n({code:"invalidRangeTooManyValues",message:"the array contains too many values",index:e.valueIndex});t[T[a++]]=s}}}e.value=t.default,Object.assign(e.varResult,t)}function J(e){h(e);const t=Array.isArray(e.value)?e.value.map(e=>L(e)):Object.entries(e.value).map(([e,n])=>L(e,n));if(0===t.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:e.valueIndex});const a=t.filter(e=>e.isDefault);if(a.length>1)throw new n({code:"invalidSelectMultipleDefaults",message:"multiple default values",index:e.valueIndex});t.forEach(e=>{delete e.isDefault}),e.varResult.options=t,e.value=(a.length>0?a[0]:t[0]).name}function D(e){const a=e.lastIndex;if("{"!==e.text[e.lastIndex])throw new t(["{"],a);const s=[];for(e.lastIndex++;"}"!==e.text[e.lastIndex];){const n={};w(e),n.name=e.value,b(e),n.label=e.value,"dropdown"===e.type?I(e):b(e),n.value=e.value,s.push(n)}if(e.lastIndex++,y(e),0===s.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:a});"dropdown"===e.type&&(e.varResult.type="select",e.type="select"),e.varResult.options=s,e.value=s[0].name}function L(e,n){let t,a=!1;e.endsWith("*")&&(a=!0,e=e.slice(0,-1));const s=e.match(/^(\w+):(.*)/);return s&&([,t,e]=s),t||(t=e),n||(n=t),{name:t,label:e,value:n,isDefault:a}}function A(e,n){if(n)try{e()}catch(e){n.push(e)}else e()}function V(e){let t;try{t=new S(e.value)}catch(n){throw n.args=[e.value],n.index=e.valueIndex,n}if(!/^https?:/.test(t.protocol))throw new n({code:"invalidURLProtocol",args:[t.protocol],message:`Invalid protocol: ${t.protocol}`,index:e.valueIndex})}function _(e){const t=e.value;if("number"!=typeof t)throw new n({code:"invalidRangeDefault",message:`the default value of @var ${e.type} must be a number`,index:e.valueIndex,args:[e.type]});const a=e.varResult;if(null!=a.min&&ta.max)throw new n({code:"invalidRangeMax",message:"the value is larger than the maximum",index:e.valueIndex});if(null!=a.step){const s=a.step.toString().split(".")[1],l=s?10**s.length:0;if(t*l%(a.step*l))throw new n({code:"invalidRangeStep",message:"the value is not a multiple of the step",index:e.valueIndex})}}function K({unknownKey:e="ignore",mandatoryKeys:t=N,parseKey:a,parseVar:s,validateKey:l,validateVar:r,allowErrors:i=!1}={}){if(!["ignore","assign","throw"].includes(e))throw new TypeError("unknownKey must be 'ignore', 'assign', or 'throw'");const o=Object.assign({__proto__:null},R,a),u=Object.assign({},j,s),d=Object.assign({},E,l),c=Object.assign({},U,r);return{parse:function(e){if(e.includes("\r"))throw new TypeError("metadata includes invalid character: '\\r'");const a={},s=[],l=/@(\w+)[^\S\r\n]*/gm,r={index:0,lastIndex:0,text:e,usercssData:a,warn:e=>s.push(e)};let o;for(;o=l.exec(e);)r.index=o.index,r.lastIndex=l.lastIndex,r.key=o[1],r.shouldIgnore=!1,A(()=>{try{"var"===r.key||"advanced"===r.key?f(r):p(r)}catch(e){throw void 0===e.index&&(e.index=r.index),e}"var"===r.key||"advanced"===r.key||r.shouldIgnore||(a[r.key]=r.value)},i&&s),l.lastIndex=r.lastIndex;return r.maybeUSO&&!a.preprocessor&&(a.preprocessor="uso"),A(()=>{const e=t.filter(e=>!Object.prototype.hasOwnProperty.call(a,e));if(e.length>0)throw new n({code:"missingMandatory",args:e,message:`Missing metadata: ${e.map(e=>`@${e}`).join(", ")}`})},i&&s),{metadata:a,errors:s}},validateVar:function(e){x({key:"var",type:e.type,value:e.value,varResult:e})}};function x(e){const n="object"==typeof c[e.type]?c[e.type][e.key]:c[e.type];n&&n(e)}function f(e){const t={type:null,label:null,name:null,value:null,default:null,options:null};e.varResult=t,m(e),e.type=e.value,t.type=e.type;const a="object"==typeof u[e.type]?u[e.type][e.key]:u[e.type];if(!a)throw new n({code:"unknownVarType",message:`Unknown @${e.key} type: ${e.type}`,args:[e.key,e.type],index:e.index});m(e),t.name=e.value,b(e),t.label=e.value,e.valueIndex=e.lastIndex,a(e),x(e),t.default=e.value,e.usercssData.vars||(e.usercssData.vars={}),e.usercssData.vars[t.name]=t,"advanced"===e.key&&(e.maybeUSO=!0)}function p(t){let a=o[t.key];if(!a){if("assign"!==e){if(v(t),"ignore"===e)return void(t.shouldIgnore=!0);throw new n({code:"unknownMeta",args:[t.key],message:`Unknown metadata: @${t.key}`,index:t.index})}a=$}t.valueIndex=t.lastIndex,a(t),d[t.key]&&d[t.key](t)}}function P({alignKeys:e=!1,space:n=2,format:t="stylus",stringifyKey:a={},stringifyVar:s={}}={}){return{stringify:function(l){let r;if("stylus"===t)r="var";else{if("xstyle"!==t)throw new TypeError("options.format must be 'stylus' or 'xstyle'");r="advanced"}const i=[];for(const[e,o]of Object.entries(l))if(Object.prototype.hasOwnProperty.call(a,e)){const n=a[e](o);Array.isArray(n)?i.push(...n.map(n=>[e,n])):i.push([e,n])}else if("vars"===e)for(const e of Object.values(o))i.push([r,z(e,t,s,n)]);else if(Array.isArray(o))for(const n of o)i.push([e,W(n)]);else i.push([e,W(o)]);const o=e?Math.max(...i.map(e=>e[0].length)):0;return`/* ==UserStyle==\n${u=i.map(([e,n])=>`@${e.padEnd(o)} ${n}`).join("\n"),u.replace(/\*\//g,"*\\/")}\n==/UserStyle== */`;var u}}}function z(e,n,t,a){return`${"xstyle"===n&&"select"===e.type?"dropdown":e.type} ${e.name} ${JSON.stringify(e.label)} ${function(){if(Object.prototype.hasOwnProperty.call(t,e.type))return t[e.type](e,n,a);if(e.options)return"stylus"===n?JSON.stringify(e.options.reduce((n,t)=>{const a=t.name===e.default?"*":"";return n[`${t.name}:${t.label}${a}`]=t.value,n},{}),null,a):function(e,n=!1,t=0){const a="string"==typeof t?t:" ".repeat(t);return`{\n${e.map(e=>`${a}${e.name} ${JSON.stringify(e.label)} ${function(e){return n?JSON.stringify(e):`<<`'${e}'`).join(", ")}`,index:n})}}class a extends n{constructor(e){super({code:"EOF",message:"Unexpected end of file",index:e})}}const s=/<<e[1]===n?n:JSON.parse(`"${e}"`))}function v(e){l.lastIndex=e.lastIndex,l.exec(e.text),e.lastIndex=l.lastIndex}function y(e){i.lastIndex=e.lastIndex,e.lastIndex+=i.exec(e.text)[0].length}function g(e){if(e.lastIndex>=e.text.length)throw new a(e.lastIndex);e.index=e.lastIndex,e.value=e.text[e.lastIndex],e.lastIndex++,y(e)}function m(e){const t=e.lastIndex;o.lastIndex=t;const a=o.exec(e.text);if(!a)throw new n({code:"invalidWord",message:"Invalid word",index:t});e.index=t,e.value=a[1],e.lastIndex+=a[0].length}function h(e){const a=e.lastIndex;try{!function e(a){const{text:s}=a;if("{"===s[a.lastIndex]){const n={};for(a.lastIndex++,y(a);"}"!==s[a.lastIndex];){b(a);const l=a.value;if(":"!==s[a.lastIndex])throw new t([":"],a.lastIndex);if(a.lastIndex++,y(a),e(a),n[l]=a.value,","===s[a.lastIndex])a.lastIndex++,y(a);else if("}"!==s[a.lastIndex])throw new t([",","}"],a.lastIndex)}a.lastIndex++,y(a),a.value=n}else if("["===s[a.lastIndex]){const n=[];for(a.lastIndex++,y(a);"]"!==s[a.lastIndex];)if(e(a),n.push(a.value),","===s[a.lastIndex])a.lastIndex++,y(a);else if("]"!==s[a.lastIndex])throw new t([",","]"],a.lastIndex);a.lastIndex++,y(a),a.value=n}else if('"'===s[a.lastIndex]||"'"===s[a.lastIndex]||"`"===s[a.lastIndex])b(a);else if(/[-\d.]/.test(s[a.lastIndex]))O(a);else{if(m(a),!(a.value in x))throw new n({code:"unknownJSONLiteral",args:[a.value],message:`Unknown literal '${a.value}'`,index:a.index});a.value=x[a.value]}}(e)}catch(e){throw e.message=`Invalid JSON: ${e.message}`,e}e.index=a}function I(e){const t=e.lastIndex;s.lastIndex=t;const a=e.text.match(s);if(!a)throw new n({code:"missingEOT",message:"Missing EOT",index:t});e.index=t,e.lastIndex+=a[0].length,e.value=p(a[1].trim()),y(e)}function w(e){c.lastIndex=e.lastIndex;const n=c.exec(e.text);e.index=e.lastIndex,e.lastIndex=c.lastIndex,e.value=n[0].trim().replace(/\s+/g,"-")}function b(e){const t=e.lastIndex,a="`"===e.text[t]?u:d;a.lastIndex=t;const s=a.exec(e.text);if(!s)throw new n({code:"invalidString",message:"Invalid string",index:t});e.index=t,e.lastIndex+=s[0].length,e.value=f(s[1])}function O(e){const t=e.lastIndex;r.lastIndex=t;const a=r.exec(e.text);if(!a)throw new n({code:"invalidNumber",message:"Invalid number",index:t});e.index=t,e.value=Number(a[0].trim()),e.lastIndex+=a[0].length}function $(e){l.lastIndex=e.lastIndex;const n=l.exec(e.text);e.index=e.lastIndex,e.value=f(n[0].trim()),e.lastIndex=l.lastIndex}var k={eatLine:v,eatWhitespace:y,parseChar:g,parseEOT:I,parseJSON:h,parseNumber:O,parseString:b,parseStringToEnd:$,parseStringUnquoted:w,parseWord:m,unquote:f};const S=self.URL,R={name:$,version:$,namespace:$,author:$,description:$,homepageURL:$,supportURL:$,updateURL:$,license:$,preprocessor:$},E={version:function(e){if(!/\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi.test(e.value))throw new n({code:"invalidVersion",args:[e.value],message:`Invalid version: ${e.value}`,index:e.valueIndex});var t;e.value="v"===(t=e.value)[0]||"="===t[0]?t.slice(1):t},homepageURL:V,supportURL:V,updateURL:V},j={text:$,color:$,checkbox:g,select:J,dropdown:{advanced:D},image:{var:J,advanced:D},number:M,range:M},U={checkbox:function(e){if("1"!==e.value&&"0"!==e.value)throw new n({code:"invalidCheckboxDefault",message:"value must be 0 or 1",index:e.valueIndex})},number:_,range:_},N=["name","namespace","version"],T=["default","min","max","step"];function M(e){h(e);const t={min:null,max:null,step:null,units:null};if("number"==typeof e.value)t.default=e.value;else{if(!Array.isArray(e.value))throw new n({code:"invalidRange",message:"the default value must be an array or a number",index:e.valueIndex,args:[e.type]});{let a=0;for(const s of e.value)if("string"==typeof s){if(null!=t.units)throw new n({code:"invalidRangeMultipleUnits",message:"units is alredy defined",args:[e.type],index:e.valueIndex});t.units=s}else{if("number"!=typeof s&&null!==s)throw new n({code:"invalidRangeValue",message:"value must be number, string, or null",args:[e.type],index:e.valueIndex});if(a>=T.length)throw new n({code:"invalidRangeTooManyValues",message:"the array contains too many values",args:[e.type],index:e.valueIndex});t[T[a++]]=s}}}e.value=t.default,Object.assign(e.varResult,t)}function J(e){h(e);const t=Array.isArray(e.value)?e.value.map(e=>L(e)):Object.entries(e.value).map(([e,n])=>L(e,n));if(0===t.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:e.valueIndex});const a=t.filter(e=>e.isDefault);if(a.length>1)throw new n({code:"invalidSelectMultipleDefaults",message:"multiple default values",index:e.valueIndex});t.forEach(e=>{delete e.isDefault}),e.varResult.options=t,e.value=(a.length>0?a[0]:t[0]).name}function D(e){const a=e.lastIndex;if("{"!==e.text[e.lastIndex])throw new t(["{"],a);const s=[];for(e.lastIndex++;"}"!==e.text[e.lastIndex];){const n={};w(e),n.name=e.value,b(e),n.label=e.value,"dropdown"===e.type?I(e):b(e),n.value=e.value,s.push(n)}if(e.lastIndex++,y(e),0===s.length)throw new n({code:"invalidSelectEmptyOptions",message:"Option list is empty",index:a});"dropdown"===e.type&&(e.varResult.type="select",e.type="select"),e.varResult.options=s,e.value=s[0].name}function L(e,n){let t,a=!1;e.endsWith("*")&&(a=!0,e=e.slice(0,-1));const s=e.match(/^(\w+):(.*)/);return s&&([,t,e]=s),t||(t=e),n||(n=t),{name:t,label:e,value:n,isDefault:a}}function A(e,n){if(n)try{e()}catch(e){n.push(e)}else e()}function V(e){let t;try{t=new S(e.value)}catch(n){throw n.args=[e.value],n.index=e.valueIndex,n}if(!/^https?:/.test(t.protocol))throw new n({code:"invalidURLProtocol",args:[t.protocol],message:`Invalid protocol: ${t.protocol}`,index:e.valueIndex})}function _(e){const t=e.value;if("number"!=typeof t)throw new n({code:"invalidRangeDefault",message:`the default value of @var ${e.type} must be a number`,index:e.valueIndex,args:[e.type]});const a=e.varResult;if(null!=a.min&&ta.max)throw new n({code:"invalidRangeMax",message:"the value is larger than the maximum",index:e.valueIndex,args:[e.type]});if(null!=a.step){const s=a.step.toString().split(".")[1],l=s?10**s.length:0;if(t*l%(a.step*l))throw new n({code:"invalidRangeStep",message:"the value is not a multiple of the step",index:e.valueIndex,args:[e.type]})}}function K({unknownKey:e="ignore",mandatoryKeys:t=N,parseKey:a,parseVar:s,validateKey:l,validateVar:r,allowErrors:i=!1}={}){if(!["ignore","assign","throw"].includes(e))throw new TypeError("unknownKey must be 'ignore', 'assign', or 'throw'");const o=Object.assign({__proto__:null},R,a),u=Object.assign({},j,s),d=Object.assign({},E,l),c=Object.assign({},U,r);return{parse:function(e){if(e.includes("\r"))throw new TypeError("metadata includes invalid character: '\\r'");const a={},s=[],l=/@(\w+)[^\S\r\n]*/gm,r={index:0,lastIndex:0,text:e,usercssData:a,warn:e=>s.push(e)};let o;for(;o=l.exec(e);)r.index=o.index,r.lastIndex=l.lastIndex,r.key=o[1],r.shouldIgnore=!1,A(()=>{try{"var"===r.key||"advanced"===r.key?p(r):f(r)}catch(e){throw void 0===e.index&&(e.index=r.index),e}"var"===r.key||"advanced"===r.key||r.shouldIgnore||(a[r.key]=r.value)},i&&s),l.lastIndex=r.lastIndex;return r.maybeUSO&&!a.preprocessor&&(a.preprocessor="uso"),A(()=>{const e=t.filter(e=>!Object.prototype.hasOwnProperty.call(a,e));if(e.length>0)throw new n({code:"missingMandatory",args:e,message:`Missing metadata: ${e.map(e=>`@${e}`).join(", ")}`})},i&&s),{metadata:a,errors:s}},validateVar:function(e){x({key:"var",type:e.type,value:e.value,varResult:e})}};function x(e){const n="object"==typeof c[e.type]?c[e.type][e.key]:c[e.type];n&&n(e)}function p(e){const t={type:null,label:null,name:null,value:null,default:null,options:null};e.varResult=t,m(e),e.type=e.value,t.type=e.type;const a="object"==typeof u[e.type]?u[e.type][e.key]:u[e.type];if(!a)throw new n({code:"unknownVarType",message:`Unknown @${e.key} type: ${e.type}`,args:[e.key,e.type],index:e.index});m(e),t.name=e.value,b(e),t.label=e.value,e.valueIndex=e.lastIndex,a(e),x(e),t.default=e.value,e.usercssData.vars||(e.usercssData.vars={}),e.usercssData.vars[t.name]=t,"advanced"===e.key&&(e.maybeUSO=!0)}function f(t){let a=o[t.key];if(!a){if("assign"!==e){if(v(t),"ignore"===e)return void(t.shouldIgnore=!0);throw new n({code:"unknownMeta",args:[t.key],message:`Unknown metadata: @${t.key}`,index:t.index})}a=$}t.valueIndex=t.lastIndex,a(t),d[t.key]&&d[t.key](t)}}function P({alignKeys:e=!1,space:n=2,format:t="stylus",stringifyKey:a={},stringifyVar:s={}}={}){return{stringify:function(l){let r;if("stylus"===t)r="var";else{if("xstyle"!==t)throw new TypeError("options.format must be 'stylus' or 'xstyle'");r="advanced"}const i=[];for(const[e,o]of Object.entries(l))if(Object.prototype.hasOwnProperty.call(a,e)){const n=a[e](o);Array.isArray(n)?i.push(...n.map(n=>[e,n])):i.push([e,n])}else if("vars"===e)for(const e of Object.values(o))i.push([r,z(e,t,s,n)]);else if(Array.isArray(o))for(const n of o)i.push([e,W(n)]);else i.push([e,W(o)]);const o=e?Math.max(...i.map(e=>e[0].length)):0;return`/* ==UserStyle==\n${u=i.map(([e,n])=>`@${e.padEnd(o)} ${n}`).join("\n"),u.replace(/\*\//g,"*\\/")}\n==/UserStyle== */`;var u}}}function z(e,n,t,a){return`${"xstyle"===n&&"select"===e.type?"dropdown":e.type} ${e.name} ${JSON.stringify(e.label)} ${function(){if(Object.prototype.hasOwnProperty.call(t,e.type))return t[e.type](e,n,a);if(e.options)return"stylus"===n?JSON.stringify(e.options.reduce((n,t)=>{const a=t.name===e.default?"*":"";return n[`${t.name}:${t.label}${a}`]=t.value,n},{}),null,a):function(e,n=!1,t=0){const a="string"==typeof t?t:" ".repeat(t);return`{\n${e.map(e=>`${a}${e.name} ${JSON.stringify(e.label)} ${function(e){return n?JSON.stringify(e):`<< Date: Wed, 26 Sep 2018 11:37:50 +0800 Subject: [PATCH 27/38] Fix: a better way to draw character list? --- js/usercss.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/usercss.js b/js/usercss.js index 6be1e3e2..1f7a4df4 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -32,7 +32,7 @@ var usercss = (() => { return backgroundWorker.parseUsercssMeta(match[0], match.index) .catch(err => { if (err.code) { - const args = ERR_ARGS_IS_LIST.has(err.code) ? err.args.join(', ') : err.args; + 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; @@ -49,6 +49,10 @@ var usercss = (() => { }); } + function drawList(chars) { + return chars.map(c => c === '"' ? "'\"'" : `"${c}"`).join(', '); + } + /** * @param {Object} style * @param {Boolean} [allowErrors=false] From a849fd6ddabf81ebde74d7c16dcd240249fbe289 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 11:39:53 +0800 Subject: [PATCH 28/38] Fix: missing placeholders --- _locales/en/messages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 654bbd29..5d31b376 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -790,7 +790,7 @@ "message": "Invalid URL protocol. Only http and https are allowed: $protocol$", "description": "Error displayed when the protocol of the URL is invalid", "placeholders": { - "type": { + "protocol": { "content": "$1" } } @@ -799,7 +799,7 @@ "message": "Invalid version number. The value doesn't match SemVer pattern: $version$", "description": "Error displayed when @version is invalid", "placeholders": { - "type": { + "version": { "content": "$1" } } From 79833d8bba5994f30398d32d238fbe98854714d9 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 11:40:04 +0800 Subject: [PATCH 29/38] Fix: a better way to draw list? --- js/usercss.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/usercss.js b/js/usercss.js index 1f7a4df4..55f5698c 100644 --- a/js/usercss.js +++ b/js/usercss.js @@ -49,8 +49,8 @@ var usercss = (() => { }); } - function drawList(chars) { - return chars.map(c => c === '"' ? "'\"'" : `"${c}"`).join(', '); + function drawList(items) { + return items.map(i => i.length === 1 ? JSON.stringify(i) : i).join(', '); } /** From 6909c73c698a81d4897af3a54cd73663ac012559 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 26 Sep 2018 12:16:33 +0800 Subject: [PATCH 30/38] Fix: minor --- _locales/en/messages.json | 2 +- edit/codemirror-default.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5d31b376..8fa5de10 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -826,7 +826,7 @@ } }, "meta_missingEOT": { - "message": "Expect EOT list", + "message": "Expect EOT data", "description": "Error displayed when the value is expected to be an EOT list" }, "meta_missingMandatory": { diff --git a/edit/codemirror-default.js b/edit/codemirror-default.js index 74fc2942..c3d1a993 100644 --- a/edit/codemirror-default.js +++ b/edit/codemirror-default.js @@ -373,9 +373,7 @@ CodeMirror.hint && (() => { // USO vars in usercss mode editor const vars = editor.getStyle().usercssData.vars; const list = vars ? - Object.keys(editor.getStyle().usercssData.vars) - .filter(name => name.startsWith(leftPart)) : - []; + Object.keys(vars).filter(name => name.startsWith(leftPart)) : []; return { list, from: {line, ch: prev}, From cc2980b647547696f591cbd8731ab880275ede94 Mon Sep 17 00:00:00 2001 From: eight Date: Mon, 1 Oct 2018 22:30:16 +0800 Subject: [PATCH 31/38] Drop: parserlib-loader --- background/parserlib-loader.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 background/parserlib-loader.js diff --git a/background/parserlib-loader.js b/background/parserlib-loader.js deleted file mode 100644 index 8275e8fd..00000000 --- a/background/parserlib-loader.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global importScripts parserlib CSSLint 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)); -}; From 268e1716b4bba9456d7fba7b742ce03ca1e93941 Mon Sep 17 00:00:00 2001 From: eight Date: Mon, 1 Oct 2018 22:38:06 +0800 Subject: [PATCH 32/38] Change: switch to worker-util --- edit.html | 3 +- edit/edit.js | 8 +++-- edit/editor-worker-body.js | 64 +++++--------------------------------- edit/editor-worker.js | 39 ----------------------- 4 files changed, 15 insertions(+), 99 deletions(-) delete mode 100644 edit/editor-worker.js diff --git a/edit.html b/edit.html index 8463a984..8787c2dc 100644 --- a/edit.html +++ b/edit.html @@ -24,6 +24,7 @@ + @@ -96,8 +97,6 @@ - -