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,165 +1,109 @@
/* 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',
};
const parser = new parserlib.css.Parser();
const sectionStack = [{code: '', start: 0}];
const errors = [];
const sections = [];
function parseMozFormat(mozStyle) { parser.addListener('startdocument', e => {
return new Promise((resolve, reject) => { const lastSection = sectionStack[sectionStack.length - 1];
const parser = new parserlib.css.Parser(); let outerText = mozStyle.slice(lastSection.start, e.offset);
const lines = mozStyle.split('\n'); const lastCmt = getLastComment(outerText);
const sectionStack = [{code: '', start: {line: 1, col: 1}}]; const section = {
const errors = []; code: '',
const sections = []; start: parser._tokenStream._token.offset + 1,
};
parser.addListener('startdocument', e => { // move last comment before @-moz-document inside the section
const lastSection = sectionStack[sectionStack.length - 1]; if (!lastCmt.includes('AGENT_SHEET') &&
let outerText = getRange(lastSection.start, {line: e.line, col: e.col - 1}); !lastCmt.includes('==') &&
const lastCmt = getLastComment(outerText); !/==userstyle==/iu.test(lastCmt)) {
const {endLine: line, endCol: col} = parser._tokenStream._token; if (lastCmt) {
const section = {code: '', start: {line, col}}; section.code = lastCmt + '\n';
// move last comment before @-moz-document inside the section outerText = outerText.slice(0, -lastCmt.length);
if (!/\/\*[\s\n]*AGENT_SHEET[\s\n]*\*\//.test(lastCmt)) {
if (lastCmt) {
section.code = lastCmt + '\n';
outerText = outerText.slice(0, -lastCmt.length);
}
outerText = outerText.match(/^\s*/)[0] + outerText.trim();
}
if (outerText.trim()) {
lastSection.code = outerText;
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);
}
sectionStack.push(section);
});
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;
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);
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.+$/, ''));
});
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)));
}
} }
outerText = outerText.match(/^\s*/)[0] + outerText.trim();
}
if (outerText.trim()) {
lastSection.code = outerText;
doAddSection(lastSection);
lastSection.code = '';
}
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);
});
function doAddSection(section) { parser.addListener('enddocument', e => {
section.code = section.code.trim(); const section = sectionStack.pop();
// don't add empty sections const lastSection = sectionStack[sectionStack.length - 1];
if ( section.code += mozStyle.slice(section.start, e.offset);
!section.code && lastSection.start = e.offset + 1;
!section.urls && doAddSection(section);
!section.urlPrefixes && });
!section.domains &&
!section.regexps
) {
return;
}
/* ignore boilerplate NS */
if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
return;
}
sections.push(Object.assign({}, section));
}
function unquote(s) { parser.addListener('endstylesheet', () => {
const first = s.charAt(0); // add nonclosed outer sections (either broken or the last global one)
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s; const lastSection = sectionStack[sectionStack.length - 1];
} lastSection.code += mozStyle.slice(lastSection.start);
sectionStack.forEach(doAddSection);
});
function getLastComment(text) { parser.addListener('error', e => {
let open = text.length; errors.push(`${e.line}:${e.col} ${e.message.replace(/ at line \d.+$/, '')}`);
let close; });
while (open) {
// at this point we're guaranteed to be outside of a comment parser.parse(mozStyle);
close = text.lastIndexOf('*/', open - 2); return {sections, errors};
if (close < 0) {
break; function doAddSection(section) {
} section.code = section.code.trim();
// stop if a non-whitespace precedes and return what we currently have // don't add empty sections
const tailEmpty = !text.substring(close + 2, open).trim(); if (
if (!tailEmpty) { !section.code &&
break; !section.urls &&
} !section.urlPrefixes &&
// find a closed preceding comment !section.domains &&
const prevClose = text.lastIndexOf('*/', close); !section.regexps
// then find the real start of current comment ) {
// e.g. /* preceding */ /* current /* current /* current */ return;
open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2); }
} /* ignore boilerplate NS */
return text.substr(open); if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
} return;
}); }
sections.push(Object.assign({}, section));
} }
return { function getLastComment(text) {
// Parse mozilla-format userstyle into sections let open = text.length;
parse(text) { let close;
return Promise.resolve(self.CSSLint || loadScript('/vendor-overwrites/csslint/csslint-worker.js')) while (open) {
.then(() => parseMozFormat(text)); // at this point we're guaranteed to be outside of a comment
}, close = text.lastIndexOf('*/', open - 2);
format(style) { if (close < 0) {
return style.sections.map(section => { break;
let cssMds = []; }
for (const i in propertyToCss) { // stop if a non-whitespace precedes and return what we currently have
if (section[i]) { const tailEmpty = !text.substring(close + 2, open).trim();
cssMds = cssMds.concat(section[i].map(v => if (!tailEmpty) {
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")' break;
)); }
} // find a closed preceding comment
} const prevClose = text.lastIndexOf('*/', close - 2);
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code; // then find the real start of current comment
}).join('\n\n'); // e.g. /* preceding */ /* current /* current /* current */
open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2);
} }
}; return open ? text.slice(open) : text;
})(); }
}

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