Add CSSLint rule configuration

This commit is contained in:
Rob Garrison 2017-08-23 17:13:55 -05:00
parent b178d3d8ee
commit ac1ffa98b5
5 changed files with 191 additions and 91 deletions

View File

@ -346,7 +346,7 @@
"description": "Label for the CSS linter issues block on the style edit page" "description": "Label for the CSS linter issues block on the style edit page"
}, },
"issuesHelp": { "issuesHelp": {
"message": "The issues found by $link$ with these rules enabled:", "message": "The issues found by $link$ rules:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page", "description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": { "placeholders": {
"link": { "link": {
@ -510,21 +510,30 @@
"message": "Remove section", "message": "Remove section",
"description": "Label for the button to remove a section" "description": "Label for the button to remove a section"
}, },
"setStylelintLink": { "setLinterLink": {
"message": "Get a full list of rules", "message": "See a full list of rules",
"description": "Stylelint rules label before link" "description": "Stylelint or CSSLint rules label added before a link"
}, },
"setStylelintRules": { "setLinterRulesTitle": {
"message": "Set stylelint rules", "message": "Set rules for $linter$",
"description": "Stylelint popup header" "description": "Stylelint or CSSLint popup header",
"placeholders": {
"linter": {
"content": "$1"
}
}
}, },
"resetStylelintRules": { "resetLinterRules": {
"message": "Reset", "message": "Reset",
"description": "Reset stylelint rules" "description": "Reset Stylelint or CSSLint rules"
}, },
"setStylelintError": { "setLinterError": {
"message": "Invalid JSON format", "message": "Invalid JSON format",
"description": "Stylelint invalid JSON message" "description": "Setting linter rules with invalid JSON message"
},
"showCSSLintSettings": {
"message": "(Set rules: 0 = disabled; 1 = warning; 2 = error)",
"description": "CSSLint rule settings values"
}, },
"shortcuts": { "shortcuts": {
"message": "Shortcuts", "message": "Shortcuts",

View File

@ -190,7 +190,7 @@
<option value="null" i18n-text="genericDisabledLabel"></option> <option value="null" i18n-text="genericDisabledLabel"></option>
</select> </select>
<span class="linter-settings" i18n-title="stylelintConfig"> <span class="linter-settings" i18n-title="stylelintConfig">
<svg id="stylelint-settings" class="svg-icon settings"> <svg id="linter-settings" class="svg-icon settings">
<use xlink:href="#svg-icon-settings"/> <use xlink:href="#svg-icon-settings"/>
</svg>&nbsp; </svg>&nbsp;
</span> </span>

49
edit/csslint-ruleset.js Normal file
View File

@ -0,0 +1,49 @@
'use strict';
/**
* CSSLint Ruleset values
* 0 = disabled; 1 = warning; 2 = error
*/
window.csslintDefaultRuleset = {
// Default warnings
'display-property-grouping': 1,
'duplicate-properties': 1,
'empty-rules': 1,
'errors': 1,
'known-properties': 1,
// Default disabled
'adjoining-classes': 0,
'box-model': 0,
'box-sizing': 0,
'bulletproof-font-face': 0,
'compatible-vendor-prefixes': 0,
'duplicate-background-images': 0,
'fallback-colors': 0,
'floats': 0,
'font-faces': 0,
'font-sizes': 0,
'gradients': 0,
'ids': 0,
'import': 0,
'import-ie-limit': 0,
'important': 0,
'order-alphabetical': 0,
'outline-none': 0,
'overqualified-elements': 0,
'qualified-headings': 0,
'regex-selectors': 0,
'rules-count': 0,
'selector-max': 0,
'selector-max-approaching': 0,
'selector-newline': 0,
'shorthand': 0,
'star-property-hack': 0,
'text-indent': 0,
'underscore-property-hack': 0,
'unique-headings': 0,
'universal-selector': 0,
'unqualified-attributes': 0,
'vendor-prefix': 0,
'zero-units': 0
};

View File

@ -1,18 +1,20 @@
/* global CodeMirror CSSLint editors makeSectionVisible showHelp showCodeMirrorPopup */ /* global CodeMirror CSSLint editors makeSectionVisible showHelp showCodeMirrorPopup */
/* global stylelintDefaultConfig onDOMscripted injectCSS require */ /* global stylelintDefaultConfig csslintDefaultRuleset onDOMscripted injectCSS require */
'use strict'; 'use strict';
function initLint() { function initLint() {
$('#lint-help').addEventListener('click', showLintHelp); $('#lint-help').addEventListener('click', showLintHelp);
$('#lint').addEventListener('click', gotoLintIssue); $('#lint').addEventListener('click', gotoLintIssue);
window.addEventListener('resize', resizeLintReport); window.addEventListener('resize', resizeLintReport);
$('#stylelint-settings').addEventListener('click', openStylelintSettings); $('#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 rules
BG.chromeLocal.getValue('editorStylelintRules').then(rules => setStylelintRules(rules)); BG.chromeLocal.getValue('editorStylelintRules').then(rules => setStylelintRules(rules));
BG.chromeLocal.getValue('editorCSSLintRules').then(ruleset => setCSSLintRules(ruleset));
} }
function setStylelintRules(rules = []) { function setStylelintRules(rules = []) {
@ -23,6 +25,14 @@ function setStylelintRules(rules = []) {
return rules; return rules;
} }
function setCSSLintRules(ruleset = []) {
if (Object.keys(ruleset).length === 0 && typeof csslintDefaultRuleset !== 'undefined') {
ruleset = Object.assign({}, csslintDefaultRuleset);
}
BG.chromeLocal.setValue('editorCSSLintRules', ruleset);
return ruleset;
}
function getLinterConfigForCodeMirror(name) { function getLinterConfigForCodeMirror(name) {
return CodeMirror.lint && CodeMirror.lint[name] ? { return CodeMirror.lint && CodeMirror.lint[name] ? {
getAnnotations: CodeMirror.lint[name], getAnnotations: CodeMirror.lint[name],
@ -30,9 +40,9 @@ function getLinterConfigForCodeMirror(name) {
} : false; } : false;
} }
function updateLinter(name) { function updateLinter(linter) {
function updateEditors() { function updateEditors() {
const options = getLinterConfigForCodeMirror(name); const options = getLinterConfigForCodeMirror(linter);
CodeMirror.defaults.lint = options === 'null' ? false : options; CodeMirror.defaults.lint = options === 'null' ? false : options;
editors.forEach(cm => { editors.forEach(cm => {
// set lint to "null" to disable // set lint to "null" to disable
@ -42,15 +52,12 @@ function updateLinter(name) {
updateLintReport(cm, 200); updateLintReport(cm, 200);
}); });
} }
if (prefs.get('editor.linter') !== name) {
prefs.set('editor.linter', name);
}
// load scripts // load scripts
loadSelectedLinter(name).then(() => { loadSelectedLinter(linter).then(() => {
updateEditors(); updateEditors();
}); });
$('#stylelint-settings').style.display = name === 'stylelint' ? $('#linter-settings').style.display = linter === 'null' ?
'inline-block' : 'none'; 'none' : 'inline-block';
} }
function updateLintReport(cm, delay) { function updateLintReport(cm, delay) {
@ -82,7 +89,6 @@ function updateLintReport(cm, delay) {
let changed = false; let changed = false;
let fixedOldIssues = false; let fixedOldIssues = false;
scope.forEach(cm => { scope.forEach(cm => {
const linter = prefs.get('editor.linter');
const scopedState = cm.state.lint || {}; const scopedState = cm.state.lint || {};
const oldMarkers = scopedState.markedLast || {}; const oldMarkers = scopedState.markedLast || {};
const newMarkers = {}; const newMarkers = {};
@ -92,12 +98,9 @@ function updateLintReport(cm, delay) {
const isActiveLine = info.from.line === cm.getCursor().line; const isActiveLine = info.from.line === cm.getCursor().line;
const pos = isActiveLine ? 'cursor' : (info.from.line + ',' + info.from.ch); const pos = isActiveLine ? 'cursor' : (info.from.line + ',' + info.from.ch);
// stylelint rule added in parentheses at the end; extract it out for the stylelint info popup // stylelint rule added in parentheses at the end; extract it out for the stylelint info popup
const stylelintRule = linter === 'stylelint' ? ` data-rule ="${ const lintRuleName = info.message
info.message
.substring(info.message.lastIndexOf('('), info.message.length) .substring(info.message.lastIndexOf('('), info.message.length)
.replace(/[()]/g, '')}"` .replace(/[()]/g, '');
: '';
// csslint
const title = escapeHtml(info.message); const title = escapeHtml(info.message);
const message = title.length > 100 ? title.substr(0, 100) + '...' : title; const message = title.length > 100 ? title.substr(0, 100) + '...' : title;
if (isActiveLine || oldMarkers[pos] === message) { if (isActiveLine || oldMarkers[pos] === message) {
@ -105,7 +108,7 @@ function updateLintReport(cm, delay) {
} }
newMarkers[pos] = message; newMarkers[pos] = message;
return `<tr class="${info.severity}"> return `<tr class="${info.severity}">
<td role="severity" ${stylelintRule}> <td role="severity" data-rule="${lintRuleName}">
<div class="CodeMirror-lint-marker-${info.severity}">${info.severity}</div> <div class="CodeMirror-lint-marker-${info.severity}">${info.severity}</div>
</td> </td>
<td role="line">${info.from.line + 1}</td> <td role="line">${info.from.line + 1}</td>
@ -200,39 +203,60 @@ function toggleLintReport() {
} }
function showLintHelp() { function showLintHelp() {
const CSSLintRules = CSSLint.getRules();
const findCSSLintRule = id => CSSLintRules.find(rule => rule.id === id);
const makeLink = (url, txt) => `<a target="_blank" href="${url}">${txt}</a>`;
const linter = prefs.get('editor.linter');
const url = linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
// some CSSLint rules do not have a url
: 'https://github.com/CSSLint/csslint/issues/535';
const rules = [];
let template;
let list = '<ul class="rules">'; let list = '<ul class="rules">';
let header = ''; let header = '';
if (prefs.get('editor.linter') === 'csslint') { if (linter === 'csslint') {
header = t('issuesHelp', '<a href="https://github.com/CSSLint/csslint" target="_blank">CSSLint</a>'); header = t('issuesHelp', makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint'));
list += CSSLint.getRules().map(rule => template = ruleID => {
`<li><b><a target="_blank" href="${rule.url}">${rule.name}</a></b><br>${rule.desc}</li>` const rule = findCSSLintRule(ruleID);
).join(''); return rule ? `<li><b>${makeLink(rule.url || url, rule.name)}</b><br>${rule.desc}</li>` : '';
};
} else { } else {
const rules = []; header = t('issuesHelp', makeLink(url, 'stylelint'));
const url = 'https://stylelint.io/user-guide/rules/'; template = rule => `<li>${makeLink(url + rule, rule)}</li>`;
header = t('issuesHelp', `<a href="${url}" target="_blank">stylelint</a>`); }
// to-do: change this to a generator // to-do: change this to a generator
$$('#lint td[role="severity"]').forEach(el => { $$('#lint td[role="severity"]').forEach(el => {
const rule = el.dataset.rule; const rule = el.dataset.rule;
if (!rules.includes(rule)) { if (!rules.includes(rule)) {
list += `<li><a target="_blank" href="${url}${rule}/">${rule}</a></li>`; list += template(rule);
rules.push(rule); rules.push(rule);
} }
}); });
}
return showHelp(t('issues'), header + list + '</ul>'); return showHelp(t('issues'), header + list + '</ul>');
} }
function setupStylelintSettingsEvents(popup) { function checkLinter(linter = prefs.get('editor.linter')) {
linter = linter.toLowerCase();
if (prefs.get('editor.linter') !== linter) {
prefs.set('editor.linter', linter);
}
return linter;
}
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 json = tryJSONparse(popup.codebox.getValue()); const json = tryJSONparse(popup.codebox.getValue());
if (json && json.rules) { if (json && json.rules) {
setStylelintRules(json.rules);
// it is possible to have stylelint rules popup open & switch to csslint // it is possible to have stylelint rules popup open & switch to csslint
if (prefs.get('editor.linter') === 'stylelint') { if (linter === 'stylelint') {
updateLinter('stylelint'); setStylelintRules(json.rules);
} else {
setCSSLintRules(json.rules);
} }
updateLinter(linter);
} else { } else {
$('#help-popup .error').classList.add('show'); $('#help-popup .error').classList.add('show');
clearTimeout($('#help-popup .contents').timer); clearTimeout($('#help-popup .contents').timer);
@ -247,27 +271,42 @@ function setupStylelintSettingsEvents(popup) {
}); });
$('.reset', popup).addEventListener('click', event => { $('.reset', popup).addEventListener('click', event => {
event.preventDefault(); event.preventDefault();
const linter = checkLinter(event.target.dataset.linter);
let rules;
if (linter === 'stylelint') {
setStylelintRules(); setStylelintRules();
popup.codebox.setValue(JSON.stringify({rules: stylelintDefaultConfig.rules}, null, 2)); rules = {rules: stylelintDefaultConfig.rules};
if (prefs.get('editor.linter') === 'stylelint') { } else {
updateLinter('stylelint'); setCSSLintRules();
rules = {rules: csslintDefaultRuleset};
} }
popup.codebox.setValue(JSON.stringify(rules, null, 2));
updateLinter(linter);
}); });
} }
function openStylelintSettings() { function openStylelintSettings() {
BG.chromeLocal.getValue('editorStylelintRules').then(rules => { const linter = prefs.get('editor.linter');
BG.chromeLocal.getValue(
linter === 'stylelint'
? 'editorStylelintRules'
: 'editorCSSLintRules'
).then(rules => {
if (rules.length === 0) { if (rules.length === 0) {
rules = setStylelintRules(rules); rules = linter === 'stylelint'
? setStylelintRules(rules)
: setCSSLintRules(rules);
} }
const rulesString = JSON.stringify({rules: rules}, null, 2); const rulesString = JSON.stringify({rules: rules}, null, 2);
setupStylelintPopup(rulesString); setupLinterPopup(rulesString);
}); });
} }
function setupStylelintPopup(rules) { function setupLinterPopup(rules) {
const linter = prefs.get('editor.linter');
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
function makeButton(className, text) { function makeButton(className, text) {
return $element({tag: 'button', className, type: 'button', textContent: t(text)}); return $element({tag: 'button', className, type: 'button', textContent: t(text), dataset: {linter}});
} }
function makeLink(url, textContent) { function makeLink(url, textContent) {
return $element({tag: 'a', target: '_blank', href: url, textContent}); return $element({tag: 'a', target: '_blank', href: url, textContent});
@ -276,21 +315,27 @@ function setupStylelintPopup(rules) {
cm.setOption('mode', 'application/json'); cm.setOption('mode', 'application/json');
cm.setOption('lint', 'json'); cm.setOption('lint', 'json');
} }
const popup = showCodeMirrorPopup(t('setStylelintRules'), $element({ const popup = showCodeMirrorPopup(t('setLinterRulesTitle', linterTitle), $element({
appendChild: [ appendChild: [
$element({ $element({
tag: 'p', tag: 'p',
appendChild: [ appendChild: [
t('setStylelintLink') + ' ', t('setLinterLink') + ' ',
makeLink('https://stylelint.io/demo/', 'Stylelint') makeLink(
linter === 'stylelint'
? 'https://stylelint.io/demo/'
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
linterTitle
),
linter === 'csslint' ? ' ' + t('showCSSLintSettings') : ''
] ]
}), }),
makeButton('save', 'styleSaveLabel'), makeButton('save', 'styleSaveLabel'),
makeButton('reset', 'resetStylelintRules'), makeButton('reset', 'resetLinterRules'),
$element({ $element({
tag: 'span', tag: 'span',
className: 'error', className: 'error',
textContent: t('setStylelintError') textContent: t('setLinterError')
}) })
] ]
})); }));
@ -304,7 +349,7 @@ function setupStylelintPopup(rules) {
popup.codebox.focus(); popup.codebox.focus();
popup.codebox.setValue(rules); popup.codebox.setValue(rules);
onDOMscripted(loadJSON).then(() => setJSONMode(popup.codebox)); onDOMscripted(loadJSON).then(() => setJSONMode(popup.codebox));
setupStylelintSettingsEvents(popup); setupLinterSettingsEvents(popup);
} }
function loadSelectedLinter(name) { function loadSelectedLinter(name) {
@ -319,7 +364,10 @@ function loadSelectedLinter(name) {
); );
} }
if (name === 'csslint' && !window.CSSLint) { if (name === 'csslint' && !window.CSSLint) {
scripts.push('vendor-overwrites/csslint/csslint-worker.js'); scripts.push(
'edit/csslint-ruleset.js',
'vendor-overwrites/csslint/csslint-worker.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',

View File

@ -4,7 +4,7 @@
// Depends on csslint.js from https://github.com/stubbornella/csslint // Depends on csslint.js from https://github.com/stubbornella/csslint
/* global CodeMirror require define */ /* global CodeMirror require define */
/* global CSSLint stylelint stylelintDefaultConfig */ /* global CSSLint stylelint stylelintDefaultConfig csslintDefaultRuleset */
'use strict'; 'use strict';
(mod => { (mod => {
@ -21,28 +21,21 @@
})(CodeMirror => { })(CodeMirror => {
CodeMirror.registerHelper('lint', 'csslint', text => { CodeMirror.registerHelper('lint', 'csslint', text => {
const found = []; const found = [];
if (window.CSSLint) { if (!window.CSSLint) {
/* STYLUS: hack start (part 1) */ return found;
const rules = CSSLint.getRules();
const allowedRules = [
'display-property-grouping',
'duplicate-properties',
'empty-rules',
'errors',
'known-properties'
];
CSSLint.clearRules();
rules.forEach(rule => {
if (allowedRules.indexOf(rule.id) >= 0) {
CSSLint.addRule(rule);
} }
}); /* STYLUS: hack start (part 1) */
/* STYLUS: hack end */ return BG.chromeLocal.getValue('editorCSSLintRules').then((ruleset = csslintDefaultRuleset) => {
// csslintDefaultRuleset stored in csslint-ruleset.js & loaded by edit/lint.js
const results = CSSLint.verify(text); if (Object.keys(ruleset).length === 0) {
ruleset = Object.assign({}, csslintDefaultRuleset);
}
const results = CSSLint.verify(text, ruleset);
const messages = results.messages; const messages = results.messages;
const hslRegex = /hsla?\(\s*(-?\d+)%?\s*,\s*(-?\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%(\s*,\s*(-?\d+|-?\d*.\d+))?\s*\)/; const hslRegex = /hsla?\(\s*(-?\d+)%?\s*,\s*(-?\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%(\s*,\s*(-?\d+|-?\d*.\d+))?\s*\)/;
let message = null; let message = null;
/* STYLUS: hack end */
for (let i = 0; i < messages.length; i++) { for (let i = 0; i < messages.length; i++) {
message = messages[i]; message = messages[i];
@ -59,28 +52,29 @@
continue; continue;
} }
} }
/* STYLUS: hack end */
const startLine = message.line - 1; const startLine = message.line - 1;
const endLine = message.line - 1; const endLine = message.line - 1;
const startCol = message.col - 1; const startCol = message.col - 1;
const endCol = message.col; const endCol = message.col;
/* STYLUS: hack end */
found.push({ found.push({
from: CodeMirror.Pos(startLine, startCol), from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol), to: CodeMirror.Pos(endLine, endCol),
message: message.message, message: message.message + ` (${message.rule.id})`,
severity : message.type severity : message.type
}); });
} }
}
return found; return found;
}); });
});
CodeMirror.registerHelper('lint', 'stylelint', text => { CodeMirror.registerHelper('lint', 'stylelint', text => {
const found = []; const found = [];
window.stylelint = require('stylelint').lint; window.stylelint = require('stylelint').lint;
if (window.stylelint) { if (window.stylelint) {
return BG.chromeLocal.getValue('editorStylelintRules').then((rules = stylelintDefaultConfig.rules) => { return BG.chromeLocal.getValue('editorStylelintRules').then((rules = stylelintDefaultConfig.rules) => {
// stylelintDefaultConfig stored in stylelint-config.js & loaded by edit.html // stylelintDefaultConfig stored in stylelint-config.js & loaded by edit/lint.js
if (Object.keys(rules).length === 0) { if (Object.keys(rules).length === 0) {
rules = stylelintDefaultConfig.rules; rules = stylelintDefaultConfig.rules;
} }