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:
parent
b8506e1e45
commit
d2cba96e10
|
@ -1,3 +1,4 @@
|
|||
vendor/
|
||||
vendor-overwrites/*
|
||||
!vendor-overwrites/colorpicker
|
||||
!vendor-overwrites/csslint
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
25
edit/edit.js
25
edit/edit.js
|
@ -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));
|
||||
})
|
||||
|
|
24
edit/lint.js
24
edit/lint.js
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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==
|
||||
|
|
23
edit/util.js
23
edit/util.js
|
@ -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');
|
||||
}
|
||||
|
|
112
js/moz-parser.js
112
js/moz-parser.js
|
@ -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');
|
||||
}
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
|
12
js/prefs.js
12
js/prefs.js
|
@ -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') {
|
||||
|
|
|
@ -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);
|
||||
|
|
35
vendor-overwrites/csslint/csslint-loader.js
Normal file
35
vendor-overwrites/csslint/csslint-loader.js
Normal 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
1723
vendor-overwrites/csslint/csslint.js
Normal file
1723
vendor-overwrites/csslint/csslint.js
Normal file
File diff suppressed because it is too large
Load Diff
5963
vendor-overwrites/csslint/parserlib.js
Normal file
5963
vendor-overwrites/csslint/parserlib.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user