regroup some of lint* data and code

* all lint-related js files are prefixed by lint-
* config-related stuff is grouped in linterConfig
* CM helper is rewritten and moved in /edit now that CSSLint supports these features
* chromeSync methods that apply LZString got LZ in their names
* empty string is used for 'disabled' in linter selector
This commit is contained in:
tophf 2017-08-28 08:22:19 +03:00
parent dfc3deaf01
commit 9946f3c781
9 changed files with 199 additions and 251 deletions

View File

@ -46,7 +46,7 @@ var chromeLocal = {
var chromeSync = { var chromeSync = {
get(options) { get(options) {
return new Promise(resolve => { return new Promise(resolve => {
chrome.storage.sync.get(options, data => resolve(data)); chrome.storage.sync.get(options, resolve);
}); });
}, },
set(data) { set(data) {
@ -54,10 +54,15 @@ var chromeSync = {
chrome.storage.sync.set(data, () => resolve(data)); chrome.storage.sync.set(data, () => resolve(data));
}); });
}, },
getValue(key) { getLZValue(key) {
return chromeSync.get(key).then(data => tryJSONparse(LZString.decompressFromUTF16(data[key]))); return chromeSync.get(key).then(data => tryJSONparse(LZString.decompressFromUTF16(data[key])));
}, },
setValue(key, value) { getLZValues(keys) {
return chromeSync.get(keys).then(data =>
Object.assign({}, ...keys.map(key =>
({[key]: tryJSONparse(LZString.decompressFromUTF16(data[key]))}))));
},
setLZValue(key, value) {
return chromeSync.set({[key]: LZString.compressToUTF16(JSON.stringify(value))}); return chromeSync.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
} }
}; };

View File

@ -187,7 +187,7 @@
<select id="editor.linter"> <select id="editor.linter">
<option value="csslint" selected>CSSLint</option> <option value="csslint" selected>CSSLint</option>
<option value="stylelint">Stylelint</option> <option value="stylelint">Stylelint</option>
<option value="null" i18n-text="genericDisabledLabel"></option> <option value="" i18n-text="genericDisabledLabel"></option>
</select> </select>
<span class="linter-settings" i18n-title="linterConfigTooltip"> <span class="linter-settings" i18n-title="linterConfigTooltip">
<svg id="linter-settings" class="svg-icon settings"> <svg id="linter-settings" class="svg-icon settings">

View File

@ -1,7 +1,7 @@
/* eslint brace-style: 0, operator-linebreak: 0 */ /* eslint brace-style: 0, operator-linebreak: 0 */
/* global CodeMirror parserlib */ /* global CodeMirror parserlib */
/* global exports css_beautify onDOMscripted */ /* global exports css_beautify onDOMscripted */
/* global CSSLint initLint getLinterConfigForCodeMirror updateLintReport renderLintReport updateLinter */ /* global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter */
'use strict'; 'use strict';
let styleId = null; let styleId = null;
@ -161,8 +161,7 @@ function initCodeMirror() {
const CM = CodeMirror; const CM = CodeMirror;
const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0; const isWindowsOS = navigator.appVersion.indexOf('Windows') > 0;
// lint.js is not loaded initially // lint.js is not loaded initially
const hasLinter = typeof getLinterConfigForCodeMirror !== 'undefined' ? const hasLinter = window.linterConfig ? linterConfig.getForCodeMirror() : false;
getLinterConfigForCodeMirror(prefs.get('editor.linter')) : false;
// CodeMirror miserably fails on keyMap='' so let's ensure it's not // CodeMirror miserably fails on keyMap='' so let's ensure it's not
if (!prefs.get('editor.keyMap')) { if (!prefs.get('editor.keyMap')) {
@ -1955,7 +1954,7 @@ function showCodeMirrorPopup(title, html, options) {
foldGutter: true, foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
matchBrackets: true, matchBrackets: true,
lint: getLinterConfigForCodeMirror(prefs.get('editor.linter')), lint: linterConfig.getForCodeMirror(),
styleActiveLine: true, styleActiveLine: true,
theme: prefs.get('editor.theme'), theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap') keyMap: prefs.get('editor.keyMap')

View File

@ -0,0 +1,31 @@
/* global CodeMirror CSSLint stylelint linterConfig */
'use strict';
CodeMirror.registerHelper('lint', 'csslint', code =>
CSSLint.verify(code, linterConfig.getCurrent('csslint'))
.messages.map(message => ({
from: CodeMirror.Pos(message.line - 1, message.col - 1),
to: CodeMirror.Pos(message.line - 1, message.col),
message: message.message + ` (${message.rule.id})`,
severity : message.type
}))
);
CodeMirror.registerHelper('lint', 'stylelint', code =>
stylelint.lint({
code,
config: linterConfig.getCurrent('stylelint'),
}).then(({results}) => {
if (!results[0]) {
return [];
}
return results[0].warnings.map(warning => ({
from: CodeMirror.Pos(warning.line - 1, warning.column - 1),
to: CodeMirror.Pos(warning.line - 1, warning.column),
message: warning.text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase()),
severity : warning.severity
}));
})
);

View File

@ -4,7 +4,7 @@
* CSSLint Config values * CSSLint Config values
* 0 = disabled; 1 = warning; 2 = error * 0 = disabled; 1 = warning; 2 = error
*/ */
window.csslintDefaultConfig = { window.linterConfig.defaults.csslint = {
// Default warnings // Default warnings
'display-property-grouping': 1, 'display-property-grouping': 1,
'duplicate-properties': 1, 'duplicate-properties': 1,

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
window.stylelintDefaultConfig = (defaultSeverity => ({ window.linterConfig.defaults.stylelint = (defaultSeverity => ({
// 'sugarss' is a indent-based syntax like Sass or Stylus // 'sugarss' is a indent-based syntax like Sass or Stylus
// ref: https://github.com/postcss/postcss#syntaxes // ref: https://github.com/postcss/postcss#syntaxes
syntax: 'sugarss', syntax: 'sugarss',

View File

@ -1,51 +1,141 @@
/* global CodeMirror messageBox */ /* global CodeMirror messageBox */
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */ /* global editors makeSectionVisible showCodeMirrorPopup showHelp */
/* global stylelintDefaultConfig csslintDefaultConfig onDOMscripted injectCSS require */ /* global onDOMscripted injectCSS require CSSLint stylelint */
'use strict'; 'use strict';
// eslint-disable-next-line no-var
var linterConfig = {
csslint: {},
stylelint: {},
defaults: {
// set in lint-defaults-csslint.js
csslint: {},
// set in lint-defaults-stylelint.js
stylelint: {},
},
storageName: {
csslint: 'editorCSSLintConfig',
stylelint: 'editorStylelintConfig',
},
getCurrent(linter = prefs.get('editor.linter')) {
return this.fallbackToDefaults(this[linter] || {});
},
getForCodeMirror(linter = prefs.get('editor.linter')) {
return CodeMirror.lint && CodeMirror.lint[linter] ? {
getAnnotations: CodeMirror.lint[linter],
delay: prefs.get('editor.lintDelay'),
} : false;
},
fallbackToDefaults(config, linter = prefs.get('editor.linter')) {
if (config && Object.keys(config).length) {
if (linter === 'stylelint') {
// always use default syntax because we don't expose it in config UI
config.syntax = this.defaults.stylelint.syntax;
}
return config;
} else {
return deepCopy(this.defaults[linter] || {});
}
},
setLinter(linter = prefs.get('editor.linter')) {
linter = linter.toLowerCase();
linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
if (prefs.get('editor.linter') !== linter) {
prefs.set('editor.linter', linter);
}
return linter;
},
findInvalidRules(config, linter = prefs.get('editor.linter')) {
const rules = linter === 'stylelint' ? config.rules : config;
const allRules = new Set(
linter === 'stylelint'
? Object.keys(stylelint.rules)
: CSSLint.getRules().map(rule => rule.id)
);
return Object.keys(rules).filter(rule => !allRules.has(rule));
},
stringify(config = this.getCurrent()) {
if (prefs.get('editor.linter') === 'stylelint') {
config.syntax = undefined;
}
return JSON.stringify(config, null, 2)
.replace(/,\n\s+\{\n\s+("severity":\s"\w+")\n\s+\}/g, ', {$1}');
},
save(config) {
config = this.fallbackToDefaults(config);
const linter = prefs.get('editor.linter');
this[linter] = config;
BG.chromeSync.setLZValue(this.storageName[linter], config);
return config;
},
loadAll() {
return BG.chromeSync.getLZValues([
'editorCSSLintConfig',
'editorStylelintConfig',
]).then(data => {
this.csslint = this.fallbackToDefaults(data.editorCSSLintConfig, 'csslint');
this.stylelint = this.fallbackToDefaults(data.editorStylelintConfig, 'stylelint');
});
},
watchStorage() {
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync') {
for (const name of ['editorCSSLintConfig', 'editorStylelintConfig']) {
if (name in changes && changes[name].newValue !== changes[name].oldValue) {
this.loadAll().then(() => debounce(updateLinter));
break;
}
}
}
});
},
// this is an event listener so it can't refer to self via 'this'
openOnClick() {
setupLinterPopup(linterConfig.stringify());
},
showSavedMessage() {
$('#help-popup .saved-message').classList.add('show');
clearTimeout($('#help-popup .contents').timer);
$('#help-popup .contents').timer = setTimeout(() => {
// popup may be closed at this point
const msg = $('#help-popup .saved-message');
if (msg) {
msg.classList.remove('show');
}
}, 2000);
},
};
function initLint() { function initLint() {
$('#lint-help').addEventListener('click', showLintHelp); $('#lint-help').addEventListener('click', showLintHelp);
$('#lint').addEventListener('click', gotoLintIssue); $('#lint').addEventListener('click', gotoLintIssue);
$('#linter-settings').addEventListener('click', linterConfig.openOnClick);
window.addEventListener('resize', resizeLintReport); window.addEventListener('resize', resizeLintReport);
$('#linter-settings').addEventListener('click', openStylelintSettings);
// touch devices don't have onHover events so the element we'll be toggled via clicking (touching) // touch devices don't have onHover events so the element we'll be toggled via clicking (touching)
if ('ontouchstart' in document.body) { if ('ontouchstart' in document.body) {
$('#lint h2').addEventListener('click', toggleLintReport); $('#lint h2').addEventListener('click', toggleLintReport);
} }
// initialize storage of linter config
BG.chromeSync.getValue('editorStylelintConfig').then(config => setStylelintConfig(config)); linterConfig.loadAll();
BG.chromeSync.getValue('editorCSSLintConfig').then(config => setCSSLintConfig(config)); linterConfig.watchStorage();
} }
function setStylelintConfig(config) { function updateLinter(linter = prefs.get('editor.linter')) {
// can't use default parameters, because config may be null
if (Object.keys(config || []).length === 0 && typeof stylelintDefaultConfig !== 'undefined') {
config = deepCopy(stylelintDefaultConfig.rules);
}
BG.chromeSync.setValue('editorStylelintConfig', config);
return config;
}
function setCSSLintConfig(config) {
if (Object.keys(config || []).length === 0 && typeof csslintDefaultConfig !== 'undefined') {
config = Object.assign({}, csslintDefaultConfig);
}
BG.chromeSync.setValue('editorCSSLintConfig', config);
return config;
}
function getLinterConfigForCodeMirror(name) {
return CodeMirror.lint && CodeMirror.lint[name] ? {
getAnnotations: CodeMirror.lint[name],
delay: prefs.get('editor.lintDelay')
} : false;
}
function updateLinter(linter) {
function updateEditors() { function updateEditors() {
const options = getLinterConfigForCodeMirror(linter); const options = linterConfig.getForCodeMirror(linter);
CodeMirror.defaults.lint = options === 'null' ? false : options; CodeMirror.defaults.lint = options;
editors.forEach(cm => { editors.forEach(cm => {
// set lint to "null" to disable // set lint to "null" to disable
cm.setOption('lint', options); cm.setOption('lint', options);
@ -58,7 +148,7 @@ function updateLinter(linter) {
loadSelectedLinter(linter).then(() => { loadSelectedLinter(linter).then(() => {
updateEditors(); updateEditors();
}); });
$('#linter-settings').style.display = linter === 'null' ? 'none' : 'inline-block'; $('#linter-settings').style.display = !linter ? 'none' : 'inline-block';
} }
function updateLintReport(cm, delay) { function updateLintReport(cm, delay) {
@ -215,7 +305,7 @@ function showLintHelp() {
let list = '<ul class="rules">'; let list = '<ul class="rules">';
let header = ''; let header = '';
if (linter === 'csslint') { if (linter === 'csslint') {
const CSSLintRules = window.CSSLint.getRules(); const CSSLintRules = CSSLint.getRules();
const findCSSLintRule = id => CSSLintRules.find(rule => rule.id === id); const findCSSLintRule = id => CSSLintRules.find(rule => rule.id === id);
header = t('linterIssuesHelp', makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint')); header = t('linterIssuesHelp', makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'));
template = ruleID => { template = ruleID => {
@ -246,64 +336,27 @@ function showLinterErrorMessage(title, contents) {
}); });
} }
function showSavedMessage() {
$('#help-popup .saved-message').classList.add('show');
clearTimeout($('#help-popup .contents').timer);
$('#help-popup .contents').timer = setTimeout(() => {
// popup may be closed at this point
const msg = $('#help-popup .saved-message');
if (msg) {
msg.classList.remove('show');
}
}, 2000);
}
function checkLinter(linter = prefs.get('editor.linter')) {
linter = linter.toLowerCase();
if (prefs.get('editor.linter') !== linter) {
prefs.set('editor.linter', linter);
}
return linter;
}
function checkConfigRules(linter, config) {
const invalid = [];
const linterRules = linter === 'stylelint'
? Object.keys(window.stylelint.rules)
: window.CSSLint.getRules().map(rule => rule.id);
Object.keys(config).forEach(setting => {
if (!linterRules.includes(setting)) {
invalid.push(setting);
}
});
return invalid;
}
function stringifyConfig(config) {
return JSON.stringify(config, null, 2)
.replace(/,\n\s+\{\n\s+("severity":\s"\w+")\n\s+\}/g, ', {$1}');
}
function setupLinterSettingsEvents(popup) { function setupLinterSettingsEvents(popup) {
$('.save', popup).addEventListener('click', event => { $('.save', popup).addEventListener('click', event => {
event.preventDefault(); event.preventDefault();
const linter = checkLinter(event.target.dataset.linter); const linter = linterConfig.setLinter(event.target.dataset.linter);
const json = tryJSONparse(popup.codebox.getValue()); const json = tryJSONparse(popup.codebox.getValue());
if (json) { if (json) {
const invalid = checkConfigRules(linter, json); const invalid = linterConfig.findInvalidRules(json, linter);
if (invalid.length) { if (invalid.length) {
return showLinterErrorMessage( showLinterErrorMessage(linter, [
linter, t('linterInvalidConfigError'),
t('linterInvalidConfigError') + `<ul><li>${invalid.join('</li><li>')}</li></ul>` $element({
); tag: 'ul',
appendChild: invalid.map(name =>
$element({tag: 'li', textContent: name})),
}),
]);
return;
} }
if (linter === 'stylelint') { linterConfig.save(json);
setStylelintConfig(json); linterConfig.showSavedMessage();
} else { debounce(updateLinter, 0, linter);
setCSSLintConfig(json);
}
updateLinter(linter);
showSavedMessage();
} else { } else {
showLinterErrorMessage(linter, t('linterJSONError')); showLinterErrorMessage(linter, t('linterJSONError'));
} }
@ -311,16 +364,8 @@ function setupLinterSettingsEvents(popup) {
}); });
$('.reset', popup).addEventListener('click', event => { $('.reset', popup).addEventListener('click', event => {
event.preventDefault(); event.preventDefault();
const linter = checkLinter(event.target.dataset.linter); const linter = linterConfig.setLinter(event.target.dataset.linter);
let config; popup.codebox.setValue(linterConfig.stringify(linterConfig.defaults[linter] || {}));
if (linter === 'stylelint') {
setStylelintConfig();
config = stylelintDefaultConfig.rules;
} else {
setCSSLintConfig();
config = csslintDefaultConfig;
}
popup.codebox.setValue(stringifyConfig(config));
popup.codebox.focus(); popup.codebox.focus();
}); });
$('.cancel', popup).addEventListener('click', event => { $('.cancel', popup).addEventListener('click', event => {
@ -329,23 +374,6 @@ function setupLinterSettingsEvents(popup) {
}); });
} }
function openStylelintSettings() {
const linter = prefs.get('editor.linter');
BG.chromeSync.getValue(
linter === 'stylelint'
? 'editorStylelintConfig'
: 'editorCSSLintConfig'
).then(config => {
if (!config || config.length === 0) {
config = linter === 'stylelint'
? setStylelintConfig(config)
: setCSSLintConfig(config);
}
const configString = stringifyConfig(config);
setupLinterPopup(configString);
});
}
function setupLinterPopup(config) { function setupLinterPopup(config) {
const linter = prefs.get('editor.linter'); const linter = prefs.get('editor.linter');
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint'; const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
@ -406,26 +434,25 @@ function setupLinterPopup(config) {
function loadSelectedLinter(name) { function loadSelectedLinter(name) {
const scripts = []; const scripts = [];
if (name !== 'null' && !$('script[src*="css-lint.js"]')) {
// inject css
injectCSS('vendor/codemirror/addon/lint/lint.css');
injectCSS('msgbox/msgbox.css');
// load CodeMirror lint code
scripts.push(
'vendor/codemirror/addon/lint/lint.js',
'vendor-overwrites/codemirror/addon/lint/css-lint.js',
'msgbox/msgbox.js'
);
}
if (name === 'csslint' && !window.CSSLint) { if (name === 'csslint' && !window.CSSLint) {
scripts.push( scripts.push(
'edit/csslint-config.js', 'vendor-overwrites/csslint/csslint-worker.js',
'vendor-overwrites/csslint/csslint-worker.js' 'edit/lint-defaults-csslint.js'
); );
} else if (name === 'stylelint' && !window.stylelint) { } else if (name === 'stylelint' && !window.stylelint) {
scripts.push( scripts.push(
'vendor-overwrites/stylelint/stylelint-bundle.min.js', 'vendor-overwrites/stylelint/stylelint-bundle.min.js',
'edit/stylelint-config.js' () => (window.stylelint = require('stylelint')),
'edit/lint-defaults-stylelint.js'
);
}
if (name && !$('script[src$="vendor/codemirror/addon/lint/lint.js"]')) {
injectCSS('vendor/codemirror/addon/lint/lint.css');
injectCSS('msgbox/msgbox.css');
scripts.push(
'vendor/codemirror/addon/lint/lint.js',
'edit/lint-codemirror-helper.js',
'msgbox/msgbox.js'
); );
} }
return onDOMscripted(scripts); return onDOMscripted(scripts);

View File

@ -42,7 +42,7 @@ var prefs = new function Prefs() {
indent_conditional: true, indent_conditional: true,
}, },
'editor.lintDelay': 500, // lint gutter marker update delay, ms 'editor.lintDelay': 500, // lint gutter marker update delay, ms
'editor.linter': 'csslint', // Choose csslint or stylelint 'editor.linter': 'csslint', // 'csslint' or 'stylelint' or ''
'editor.lintReportDelay': 4500, // lint report update delay, ms 'editor.lintReportDelay': 4500, // 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

View File

@ -1,114 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Depends on csslint.js from https://github.com/stubbornella/csslint
/* global CodeMirror require define */
/* global CSSLint stylelint stylelintDefaultConfig csslintDefaultConfig */
'use strict';
(mod => {
if (typeof exports === 'object' && typeof module === 'object') {
// CommonJS
mod(require('../../lib/codemirror'));
} else if (typeof define === 'function' && define.amd) {
// AMD
define(['../../lib/codemirror'], mod);
} else {
// Plain browser env
mod(CodeMirror);
}
})(CodeMirror => {
CodeMirror.registerHelper('lint', 'csslint', text => {
const found = [];
if (!window.CSSLint) {
return found;
}
/* STYLUS: hack start (part 1) */
return BG.chromeSync.getValue('editorCSSLintConfig').then(config => {
// csslintDefaultConfig stored in csslint-config.js & loaded by edit/lint.js
if (Object.keys(config || []).length === 0) {
config = Object.assign({}, csslintDefaultConfig);
}
const results = CSSLint.verify(text, config);
const messages = results.messages;
const hslRegex = /hsla?\(\s*(-?\d+)%?\s*,\s*(-?\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%(\s*,\s*(-?\d+|-?\d*.\d+))?\s*\)/;
let message = null;
/* STYLUS: hack end */
for (let i = 0; i < messages.length; i++) {
message = messages[i];
/* STYLUS: hack start (part 2) */
if (message.type === 'warning') {
// @font-face {font-family: 'Ampersand'; unicode-range: U+26;}
if (message.message.indexOf('unicode-range') !== -1) {
continue;
} else if (
// color: hsl(210, 100%, 2.2%); or color: hsla(210, 100%, 2.2%, 0.3);
message.message.startsWith('Expected (<color>) but found \'hsl') &&
hslRegex.test(message.message)
) {
continue;
}
}
const startLine = message.line - 1;
const endLine = message.line - 1;
const startCol = message.col - 1;
const endCol = message.col;
/* STYLUS: hack end */
found.push({
from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol),
message: message.message + ` (${message.rule.id})`,
severity : message.type
});
}
return found;
});
});
CodeMirror.registerHelper('lint', 'stylelint', text => {
const found = [];
window.stylelint = require('stylelint');
if (window.stylelint) {
return BG.chromeSync.getValue('editorStylelintConfig').then(rules => {
// stylelintDefaultConfig stored in stylelint-config.js & loaded by edit/lint.js
if (Object.keys(rules || []).length === 0) {
rules = stylelintDefaultConfig.rules;
}
return stylelint.lint({
code: text,
config: {
syntax: stylelintDefaultConfig.syntax,
rules: rules
}
}).then(output => {
const warnings = output.results.length ? output.results[0].warnings : [];
const len = warnings.length;
let warning;
let message;
if (len) {
for (let i = 0; i < len; i++) {
warning = warnings[i];
message = warning.text
.replace('Unexpected ', '')
.replace(/^./, function (firstLetter) {
return firstLetter.toUpperCase();
});
found.push({
from: CodeMirror.Pos(warning.line - 1, warning.column - 1),
to: CodeMirror.Pos(warning.line - 1, warning.column),
message,
severity : warning.severity
});
}
}
return found;
});
});
}
return found;
});
});