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-overwrites/*
!vendor-overwrites/colorpicker
!vendor-overwrites/csslint

View File

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

View File

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

View File

@ -1,11 +1,12 @@
/*
global CodeMirror parserlib loadScript
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
global mozParser createSourceEditor
global createSourceEditor
global closeCurrentTab regExpTester messageBox
global setupCodeMirror
global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes
global sectionsToMozFormat
*/
'use strict';
@ -415,7 +416,7 @@ function showMozillaFormat() {
}
function toMozillaFormat() {
return mozParser.format({sections: getSectionsHashes()});
return sectionsToMozFormat({sections: getSectionsHashes()});
}
function fromMozillaFormat() {
@ -450,8 +451,24 @@ function fromMozillaFormat() {
function doImport({replaceOldStyle = false}) {
lockPageUI(true);
new Promise(setTimeout)
.then(() => mozParser.parse(popup.codebox.getValue().trim()))
.then(sections => {
.then(() => {
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);
return addSections(sections, div => setCleanItem(div, false));
})

View File

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

View File

@ -1,6 +1,6 @@
/* global CodeMirror dirtyReporter initLint */
/* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
/* global editors linterConfig updateLinter regExpTester mozParser */
/* global editors linterConfig updateLinter regExpTester sectionsToMozFormat */
/* global createAppliesToLineWidget messageBox */
'use strict';
@ -100,10 +100,10 @@ function createSourceEditor(style) {
function setupNewStyle(style) {
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')) {
style.sections[0].domains = ['example.com'];
section = mozParser.format(style);
section = sectionsToMozFormat(style);
}
const DEFAULT_CODE = `
/* ==UserStyle==

View File

@ -93,3 +93,26 @@ function dirtyReporter() {
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';
// eslint-disable-next-line no-var
var mozParser = (() => {
// direct & reverse mapping of @-moz-document keywords and internal property names
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
function parseMozFormat(mozStyle) {
return new Promise((resolve, reject) => {
function parseMozFormat(mozStyle) {
const CssToProperty = {
'url': 'urls',
'url-prefix': 'urlPrefixes',
'domain': 'domains',
'regexp': 'regexps',
};
const parser = new parserlib.css.Parser();
const lines = mozStyle.split('\n');
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
const sectionStack = [{code: '', start: 0}];
const errors = [];
const sections = [];
parser.addListener('startdocument', e => {
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 {endLine: line, endCol: col} = parser._tokenStream._token;
const section = {code: '', start: {line, col}};
const section = {
code: '',
start: parser._tokenStream._token.offset + 1,
};
// 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) {
section.code = lastCmt + '\n';
outerText = outerText.slice(0, -lastCmt.length);
@ -34,15 +36,9 @@ var mozParser = (() => {
doAddSection(lastSection);
lastSection.code = '';
}
for (const f of e.functions) {
const m = f && f.match(/^([\w-]*)\((.+?)\)$/);
if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
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);
for (const {name, expr, uri} of e.functions) {
const aType = CssToProperty[name.toLowerCase()];
(section[aType] = section[aType] || []).push(uri || expr && expr.parts[0].value || '');
}
sectionStack.push(section);
});
@ -50,48 +46,24 @@ var mozParser = (() => {
parser.addListener('enddocument', e => {
const section = sectionStack.pop();
const lastSection = sectionStack[sectionStack.length - 1];
const end = {line: e.line, col: e.col - 1};
section.code += getRange(section.start, end);
end.col += 2;
lastSection.start = end;
section.code += mozStyle.slice(section.start, e.offset);
lastSection.start = e.offset + 1;
doAddSection(section);
});
parser.addListener('endstylesheet', () => {
// 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];
lastSection.code += getRange(lastSection.start, endOfText);
lastSection.code += mozStyle.slice(lastSection.start);
sectionStack.forEach(doAddSection);
if (errors.length) {
reject(errors);
} else {
resolve(sections);
}
});
parser.addListener('error', e => {
errors.push(e.line + ':' + e.col + ' ' +
e.message.replace(/ at line \d.+$/, ''));
errors.push(`${e.line}:${e.col} ${e.message.replace(/ at line \d.+$/, '')}`);
});
parser.parse(mozStyle);
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)));
}
}
return {sections, errors};
function doAddSection(section) {
section.code = section.code.trim();
@ -112,11 +84,6 @@ var mozParser = (() => {
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) {
let open = text.length;
let close;
@ -132,34 +99,11 @@ var mozParser = (() => {
break;
}
// find a closed preceding comment
const prevClose = text.lastIndexOf('*/', close);
const prevClose = text.lastIndexOf('*/', close - 2);
// then find the real start of current comment
// e.g. /* preceding */ /* current /* current /* current */
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,
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.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
// selection = only when something is selected
// '' (empty string) = disabled
@ -234,6 +234,7 @@ var prefs = new function Prefs() {
// 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
const importFromLocalStorage = () => {
forgetOutdatedDefaults(localStorage);
for (const key in defaults) {
const defaultValue = defaults[key];
let value = localStorage[key];
@ -323,6 +324,7 @@ var prefs = new function Prefs() {
}
function importFromSync(synced = {}) {
forgetOutdatedDefaults(synced);
for (const key in defaults) {
if (key in synced) {
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) {
const copy = deepCopy(value);
if (typeof copy === 'object') {

View File

@ -1,4 +1,4 @@
/* global loadScript mozParser semverCompare colorConverter styleCodeEmpty */
/* global loadScript semverCompare colorConverter styleCodeEmpty */
'use strict';
// eslint-disable-next-line no-var
@ -485,11 +485,17 @@ var usercss = (() => {
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(sections => (style.sections = sections))
.then(() => builder.postprocess && builder.postprocess(style.sections, sVars))
.then(() => style);
.then(({sections, errors}) => sections.length && sections || Promise.reject(errors))
.then(sections => {
style.sections = sections;
if (builder.postprocess) builder.postprocess(style.sections, sVars);
return style;
}));
}
function simpleVars(vars) {
@ -568,7 +574,7 @@ var usercss = (() => {
function invokeWorker(message) {
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.instance.onmessage = ({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