refactor CSSLint

* reduce linting delay
* parse mozformat in worker
* allow empty functions in 'filter:' property
  https://drafts.fxtf.org/filter-effects/#supported-filter-functions
* support comma-separated list in :lang()
* strip vendor prefix in isLiteral()
This commit is contained in:
tophf 2017-12-26 23:39:52 +03:00
parent b8506e1e45
commit d2cba96e10
14 changed files with 7908 additions and 11329 deletions

View File

@ -1,3 +1,4 @@
vendor/ vendor/
vendor-overwrites/* vendor-overwrites/*
!vendor-overwrites/colorpicker !vendor-overwrites/colorpicker
!vendor-overwrites/csslint

View File

@ -239,13 +239,13 @@ rules:
object-curly-spacing: [2, never] object-curly-spacing: [2, never]
object-shorthand: [0] object-shorthand: [0]
one-var-declaration-per-line: [1] one-var-declaration-per-line: [1]
one-var: [0] one-var: [2, {initialized: never}]
operator-assignment: [2, always] operator-assignment: [2, always]
operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}] operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
padded-blocks: [0] padded-blocks: [0]
prefer-numeric-literals: [2] prefer-numeric-literals: [2]
prefer-rest-params: [0] prefer-rest-params: [0]
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}] prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
quote-props: [0] quote-props: [0]
quotes: [1, single, avoid-escape] quotes: [1, single, avoid-escape]
radix: [2, as-needed] radix: [2, as-needed]

View File

@ -5,6 +5,7 @@
<link href="global.css" rel="stylesheet"> <link href="global.css" rel="stylesheet">
<link href="edit/edit.css" rel="stylesheet"> <link href="edit/edit.css" rel="stylesheet">
<link rel="stylesheet" href="msgbox/msgbox.css">
<style id="firefox-transitions-bug-suppressor"> <style id="firefox-transitions-bug-suppressor">
/* restrict to FF */ /* restrict to FF */
@ -22,7 +23,6 @@
<script src="js/prefs.js"></script> <script src="js/prefs.js"></script>
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/script-loader.js"></script> <script src="js/script-loader.js"></script>
<script src="js/moz-parser.js"></script>
<script src="js/storage-util.js"></script> <script src="js/storage-util.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<script src="edit/lint.js"></script> <script src="edit/lint.js"></script>
@ -37,6 +37,8 @@
<script src="edit/codemirror-editing-hooks.js"></script> <script src="edit/codemirror-editing-hooks.js"></script>
<script src="edit/edit.js"></script> <script src="edit/edit.js"></script>
<script src="msgbox/msgbox.js" async></script>
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet"> <link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<script src="vendor/codemirror/lib/codemirror.js"></script> <script src="vendor/codemirror/lib/codemirror.js"></script>

View File

@ -1,11 +1,12 @@
/* /*
global CodeMirror parserlib loadScript global CodeMirror parserlib loadScript
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
global mozParser createSourceEditor global createSourceEditor
global closeCurrentTab regExpTester messageBox global closeCurrentTab regExpTester messageBox
global setupCodeMirror global setupCodeMirror
global beautify global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes global initWithSectionStyle addSections removeSection getSectionsHashes
global sectionsToMozFormat
*/ */
'use strict'; 'use strict';
@ -415,7 +416,7 @@ function showMozillaFormat() {
} }
function toMozillaFormat() { function toMozillaFormat() {
return mozParser.format({sections: getSectionsHashes()}); return sectionsToMozFormat({sections: getSectionsHashes()});
} }
function fromMozillaFormat() { function fromMozillaFormat() {
@ -450,8 +451,24 @@ function fromMozillaFormat() {
function doImport({replaceOldStyle = false}) { function doImport({replaceOldStyle = false}) {
lockPageUI(true); lockPageUI(true);
new Promise(setTimeout) new Promise(setTimeout)
.then(() => mozParser.parse(popup.codebox.getValue().trim())) .then(() => {
.then(sections => { const worker = linterConfig.worker.csslint;
if (!worker.instance) worker.instance = new Worker(worker.path);
})
.then(() => linterConfig.invokeWorker({
linter: 'csslint',
action: 'parse',
code: popup.codebox.getValue().trim(),
}))
.then(({sections, errors}) => {
// shouldn't happen but just in case
if (!sections.length && errors.length) {
return Promise.reject(errors);
}
// show the errors in case linting is disabled or stylelint misses what csslint has found
if (errors.length && prefs.get('editor.linter') !== 'csslint') {
showError(errors);
}
removeOldSections(replaceOldStyle); removeOldSections(replaceOldStyle);
return addSections(sections, div => setCleanItem(div, false)); return addSections(sections, div => setCleanItem(div, false));
}) })

View File

@ -20,7 +20,7 @@ var linterConfig = {
stylelint: 'editorStylelintConfig', stylelint: 'editorStylelintConfig',
}, },
worker: { worker: {
csslint: {path: '/vendor-overwrites/csslint/csslint-worker.js'}, csslint: {path: '/vendor-overwrites/csslint/csslint-loader.js'},
stylelint: {path: '/vendor-overwrites/stylelint/stylelint-bundle.min.js'}, stylelint: {path: '/vendor-overwrites/stylelint/stylelint-bundle.min.js'},
}, },
allRuleIds: { allRuleIds: {
@ -48,7 +48,7 @@ var linterConfig = {
}, },
onUpdateLinting(annotationsNotSorted, annotations, cm) { onUpdateLinting(annotationsNotSorted, annotations, cm) {
cm.endOperation(); cm.endOperation();
updateLintReport(cm, 0); updateLintReport(cm);
}, },
} : false; } : false;
}, },
@ -561,15 +561,17 @@ function setupLinterPopup(config) {
function loadLinterAssets(name = linterConfig.getName()) { function loadLinterAssets(name = linterConfig.getName()) {
const worker = linterConfig.worker[name]; const worker = linterConfig.worker[name];
return !name || !worker || worker.instance ? Promise.resolve() : if (!name || !worker) return Promise.resolve();
loadScript((worker.instance ? [] : [ const scripts = [];
(worker.instance = new Worker(worker.path)), if (!worker.instance) {
`/edit/lint-defaults-${name}.js`, worker.instance = new Worker(worker.path);
]).concat(CodeMirror.lint ? [] : [ scripts.push(`/edit/lint-defaults-${name}.js`);
}
if (!CodeMirror.lint) {
scripts.push(
'/vendor/codemirror/addon/lint/lint.css', '/vendor/codemirror/addon/lint/lint.css',
'/msgbox/msgbox.css',
'/vendor/codemirror/addon/lint/lint.js', '/vendor/codemirror/addon/lint/lint.js',
'/edit/lint-codemirror-helper.js', '/edit/lint-codemirror-helper.js');
'/msgbox/msgbox.js' }
])); return scripts.length ? loadScript(scripts) : Promise.resolve();
} }

View File

@ -1,6 +1,6 @@
/* global CodeMirror dirtyReporter initLint */ /* global CodeMirror dirtyReporter initLint */
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */ /* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
/* global editors linterConfig updateLinter regExpTester mozParser */ /* global editors linterConfig updateLinter regExpTester sectionsToMozFormat */
/* global createAppliesToLineWidget messageBox */ /* global createAppliesToLineWidget messageBox */
'use strict'; 'use strict';
@ -100,10 +100,10 @@ function createSourceEditor(style) {
function setupNewStyle(style) { function setupNewStyle(style) {
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + '/* Insert code here... */'; style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + '/* Insert code here... */';
let section = mozParser.format(style); let section = sectionsToMozFormat(style);
if (!section.includes('@-moz-document')) { if (!section.includes('@-moz-document')) {
style.sections[0].domains = ['example.com']; style.sections[0].domains = ['example.com'];
section = mozParser.format(style); section = sectionsToMozFormat(style);
} }
const DEFAULT_CODE = ` const DEFAULT_CODE = `
/* ==UserStyle== /* ==UserStyle==

View File

@ -93,3 +93,26 @@ function dirtyReporter() {
return wrap({add, remove, modify, clear, isDirty, onChange, has}); return wrap({add, remove, modify, clear, isDirty, onChange, has});
} }
function sectionsToMozFormat(style) {
const propertyToCss = {
urls: 'url',
urlPrefixes: 'url-prefix',
domains: 'domain',
regexps: 'regexp',
};
return style.sections.map(section => {
let cssMds = [];
for (const i in propertyToCss) {
if (section[i]) {
cssMds = cssMds.concat(section[i].map(v =>
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
));
}
}
return cssMds.length ?
'@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' :
section.code;
}).join('\n\n');
}

View File

@ -1,28 +1,30 @@
/* global parserlib, loadScript */ /* global parserlib */
'use strict'; 'use strict';
// eslint-disable-next-line no-var function parseMozFormat(mozStyle) {
var mozParser = (() => { const CssToProperty = {
// direct & reverse mapping of @-moz-document keywords and internal property names 'url': 'urls',
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; 'url-prefix': 'urlPrefixes',
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'}; 'domain': 'domains',
'regexp': 'regexps',
function parseMozFormat(mozStyle) { };
return new Promise((resolve, reject) => {
const parser = new parserlib.css.Parser(); const parser = new parserlib.css.Parser();
const lines = mozStyle.split('\n'); const sectionStack = [{code: '', start: 0}];
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
const errors = []; const errors = [];
const sections = []; const sections = [];
parser.addListener('startdocument', e => { parser.addListener('startdocument', e => {
const lastSection = sectionStack[sectionStack.length - 1]; const lastSection = sectionStack[sectionStack.length - 1];
let outerText = getRange(lastSection.start, {line: e.line, col: e.col - 1}); let outerText = mozStyle.slice(lastSection.start, e.offset);
const lastCmt = getLastComment(outerText); const lastCmt = getLastComment(outerText);
const {endLine: line, endCol: col} = parser._tokenStream._token; const section = {
const section = {code: '', start: {line, col}}; code: '',
start: parser._tokenStream._token.offset + 1,
};
// move last comment before @-moz-document inside the section // move last comment before @-moz-document inside the section
if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) { if (!lastCmt.includes('AGENT_SHEET') &&
!lastCmt.includes('==') &&
!/==userstyle==/iu.test(lastCmt)) {
if (lastCmt) { if (lastCmt) {
section.code = lastCmt + '\n'; section.code = lastCmt + '\n';
outerText = outerText.slice(0, -lastCmt.length); outerText = outerText.slice(0, -lastCmt.length);
@ -34,15 +36,9 @@ var mozParser = (() => {
doAddSection(lastSection); doAddSection(lastSection);
lastSection.code = ''; lastSection.code = '';
} }
for (const f of e.functions) { for (const {name, expr, uri} of e.functions) {
const m = f && f.match(/^([\w-]*)\((.+?)\)$/); const aType = CssToProperty[name.toLowerCase()];
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) { (section[aType] = section[aType] || []).push(uri || expr && expr.parts[0].value || '');
errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
continue;
}
const aType = CssToProperty[m[1]];
const aValue = unquote(aType !== 'regexps' ? m[2] : m[2].replace(/\\\\/g, '\\'));
(section[aType] = section[aType] || []).push(aValue);
} }
sectionStack.push(section); sectionStack.push(section);
}); });
@ -50,48 +46,24 @@ var mozParser = (() => {
parser.addListener('enddocument', e => { parser.addListener('enddocument', e => {
const section = sectionStack.pop(); const section = sectionStack.pop();
const lastSection = sectionStack[sectionStack.length - 1]; const lastSection = sectionStack[sectionStack.length - 1];
const end = {line: e.line, col: e.col - 1}; section.code += mozStyle.slice(section.start, e.offset);
section.code += getRange(section.start, end); lastSection.start = e.offset + 1;
end.col += 2;
lastSection.start = end;
doAddSection(section); doAddSection(section);
}); });
parser.addListener('endstylesheet', () => { parser.addListener('endstylesheet', () => {
// add nonclosed outer sections (either broken or the last global one) // add nonclosed outer sections (either broken or the last global one)
const lastLine = lines[lines.length - 1];
const endOfText = {line: lines.length, col: lastLine.length + 1};
const lastSection = sectionStack[sectionStack.length - 1]; const lastSection = sectionStack[sectionStack.length - 1];
lastSection.code += getRange(lastSection.start, endOfText); lastSection.code += mozStyle.slice(lastSection.start);
sectionStack.forEach(doAddSection); sectionStack.forEach(doAddSection);
if (errors.length) {
reject(errors);
} else {
resolve(sections);
}
}); });
parser.addListener('error', e => { parser.addListener('error', e => {
errors.push(e.line + ':' + e.col + ' ' + errors.push(`${e.line}:${e.col} ${e.message.replace(/ at line \d.+$/, '')}`);
e.message.replace(/ at line \d.+$/, ''));
}); });
parser.parse(mozStyle); parser.parse(mozStyle);
return {sections, errors};
function getRange(start, end) {
const L1 = start.line - 1;
const C1 = start.col - 1;
const L2 = end.line - 1;
const C2 = end.col - 1;
if (L1 === L2) {
return lines[L1].substr(C1, C2 - C1 + 1);
} else {
const middle = lines.slice(L1 + 1, L2).join('\n');
return lines[L1].substr(C1) + '\n' + middle +
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
}
}
function doAddSection(section) { function doAddSection(section) {
section.code = section.code.trim(); section.code = section.code.trim();
@ -112,11 +84,6 @@ var mozParser = (() => {
sections.push(Object.assign({}, section)); sections.push(Object.assign({}, section));
} }
function unquote(s) {
const first = s.charAt(0);
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
}
function getLastComment(text) { function getLastComment(text) {
let open = text.length; let open = text.length;
let close; let close;
@ -132,34 +99,11 @@ var mozParser = (() => {
break; break;
} }
// find a closed preceding comment // find a closed preceding comment
const prevClose = text.lastIndexOf('*/', close); const prevClose = text.lastIndexOf('*/', close - 2);
// then find the real start of current comment // then find the real start of current comment
// e.g. /* preceding */ /* current /* current /* current */ // e.g. /* preceding */ /* current /* current /* current */
open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2); open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2);
} }
return text.substr(open); return open ? text.slice(open) : text;
} }
}); }
}
return {
// Parse mozilla-format userstyle into sections
parse(text) {
return Promise.resolve(self.CSSLint || loadScript('/vendor-overwrites/csslint/csslint-worker.js'))
.then(() => parseMozFormat(text));
},
format(style) {
return style.sections.map(section => {
let cssMds = [];
for (const i in propertyToCss) {
if (section[i]) {
cssMds = cssMds.concat(section[i].map(v =>
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
));
}
}
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
}).join('\n\n');
}
};
})();

View File

@ -57,9 +57,9 @@ var prefs = new function Prefs() {
end_with_newline: false, end_with_newline: false,
indent_conditional: true, indent_conditional: true,
}, },
'editor.lintDelay': 500, // lint gutter marker update delay, ms 'editor.lintDelay': 300, // lint gutter marker update delay, ms
'editor.linter': 'csslint', // 'csslint' or 'stylelint' or '' 'editor.linter': 'csslint', // 'csslint' or 'stylelint' or ''
'editor.lintReportDelay': 4500, // lint report update delay, ms 'editor.lintReportDelay': 500, // lint report update delay, ms
'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected 'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
// selection = only when something is selected // selection = only when something is selected
// '' (empty string) = disabled // '' (empty string) = disabled
@ -234,6 +234,7 @@ var prefs = new function Prefs() {
// Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready, // Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
// so we'll mirror the prefs to avoid using the wrong defaults during the startup phase // so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
const importFromLocalStorage = () => { const importFromLocalStorage = () => {
forgetOutdatedDefaults(localStorage);
for (const key in defaults) { for (const key in defaults) {
const defaultValue = defaults[key]; const defaultValue = defaults[key];
let value = localStorage[key]; let value = localStorage[key];
@ -323,6 +324,7 @@ var prefs = new function Prefs() {
} }
function importFromSync(synced = {}) { function importFromSync(synced = {}) {
forgetOutdatedDefaults(synced);
for (const key in defaults) { for (const key in defaults) {
if (key in synced) { if (key in synced) {
this.set(key, synced[key], {sync: false}); this.set(key, synced[key], {sync: false});
@ -330,6 +332,12 @@ var prefs = new function Prefs() {
} }
} }
function forgetOutdatedDefaults(storage) {
// our linter runs as a worker so we can reduce the delay and forget the old default values
if (Number(storage['editor.lintDelay']) === 500) delete storage['editor.lintDelay'];
if (Number(storage['editor.lintReportDelay']) === 4500) delete storage['editor.lintReportDelay'];
}
function defineReadonlyProperty(obj, key, value) { function defineReadonlyProperty(obj, key, value) {
const copy = deepCopy(value); const copy = deepCopy(value);
if (typeof copy === 'object') { if (typeof copy === 'object') {

View File

@ -1,4 +1,4 @@
/* global loadScript mozParser semverCompare colorConverter styleCodeEmpty */ /* global loadScript semverCompare colorConverter styleCodeEmpty */
'use strict'; 'use strict';
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -485,11 +485,17 @@ var usercss = (() => {
const sVars = simpleVars(vars); const sVars = simpleVars(vars);
return Promise.resolve(builder.preprocess && builder.preprocess(sourceCode, sVars) || sourceCode) return (
Promise.resolve(
builder.preprocess && builder.preprocess(sourceCode, sVars) ||
sourceCode)
.then(mozStyle => invokeWorker({action: 'parse', code: mozStyle})) .then(mozStyle => invokeWorker({action: 'parse', code: mozStyle}))
.then(sections => (style.sections = sections)) .then(({sections, errors}) => sections.length && sections || Promise.reject(errors))
.then(() => builder.postprocess && builder.postprocess(style.sections, sVars)) .then(sections => {
.then(() => style); style.sections = sections;
if (builder.postprocess) builder.postprocess(style.sections, sVars);
return style;
}));
} }
function simpleVars(vars) { function simpleVars(vars) {
@ -568,7 +574,7 @@ var usercss = (() => {
function invokeWorker(message) { function invokeWorker(message) {
if (!worker.queue) { if (!worker.queue) {
worker.instance = new Worker('/vendor-overwrites/csslint/csslint-worker.js'); worker.instance = new Worker('/vendor-overwrites/csslint/csslint-loader.js');
worker.queue = []; worker.queue = [];
worker.instance.onmessage = ({data}) => { worker.instance.onmessage = ({data}) => {
worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data); worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);

View File

@ -0,0 +1,35 @@
/* global parserlib CSSLint parseMozFormat */
'use strict';
self.importScripts('./parserlib.js');
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
self.onmessage = ({data: {action = 'run', code, config}}) => {
if (action === 'parse') {
if (!self.parseMozFormat) self.importScripts('/js/moz-parser.js');
self.postMessage(parseMozFormat(code));
return;
}
if (!self.CSSLint) self.importScripts('./csslint.js');
switch (action) {
case 'getAllRuleIds':
// the functions are non-tranferable and we need only an id
self.postMessage(CSSLint.getRules().map(rule => rule.id));
return;
case 'getAllRuleInfos':
// the functions are non-tranferable
self.postMessage(CSSLint.getRules().map(rule => JSON.parse(JSON.stringify(rule))));
return;
case 'run': {
const results = CSSLint.verify(code, config).messages
//.filter(m => !m.message.includes('/*[[') && !m.message.includes(']]*/'))
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
self.postMessage(results);
return;
}
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff