Rewrite linter system (#487)
* Add: implement new linter system * Refactor: pull out editor worker * Switch to new linter and worker * Enable eslint cache * Fix: undefined error * Windows compatibility * Fix: refresh linter if the editor.linter changes * Add: stylelint * Add: getStylelintRules, getCsslintRules * Fix: logic to get correct linter * WIP: linter-report * Fix: toggle hidden state * Add: matain the order of lint report for section editor * Add: unhook event * Add: gotoLintIssue * Fix: shouldn't delete rule.init * Add: linter-help-dialog * Drop linterConfig * Add: linter-config-dialog, cacheFn * Add: use cacheFn * Drop lint.js * Add: refresh. Fix report order * Fix: hide empty table * Add: updateCount. Fix table caption * Switch to new linter/worker * Fix: remove unneeded comment * Fix: cacheFn -> cacheFirstCall * Fix: use cacheFirstCall * Fix: cache metaIndex * Fix: i < trs.length * Fix: drop isEmpty * Fix: expose some simple states to global * Fix: return object code style * Fix: use proxy to reflect API * Fix: eslint-disable-line -> eslint-disable-next-line * Fix: requestId -> id * Fix: one-liner * Fix: one-liner * Fix: move dom event block to top * Fix: pending -> pendingResponse * Fix: onSuccess -> onUpdated * Fix: optimize row removing when i === 0 * Fix: hook/unhook -> enableForEditor/disableForEditor * Fix: linter.refresh -> linter.run * Fix: some shadowing * Fix: simplify getAnnotations * Fix: cacheFirstCall -> memoize * Fix: table.update -> table.updateCaption * Fix: unneeded reassign * Fix: callbacks -> listeners * Fix: don't compose but extend * Refactor: replace linter modules with linter-defaults and linter-engines * Fix: implement linter fallbacks * Fix: linter.onChange -> linter.onLintingUpdated * Fix: cms -> tables * Fix: parseMozFormat is not called correctly * Move csslint-loader to background * Fix: watch config changes * Fix: switch to LINTER_DEFAULTS * Fix: csslint-loader -> parserlib-loader
This commit is contained in:
parent
5f60c519ce
commit
2fd531e253
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ node_modules/
|
|||
package-lock.json
|
||||
yarn.lock
|
||||
*.zip
|
||||
.eslintcache
|
||||
|
|
9
background/parserlib-loader.js
Normal file
9
background/parserlib-loader.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/* global importScripts parserlib CSSLint parseMozFormat */
|
||||
'use strict';
|
||||
|
||||
importScripts('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
||||
|
||||
self.onmessage = ({data}) => {
|
||||
self.postMessage(parseMozFormat(data));
|
||||
};
|
15
edit.html
15
edit.html
|
@ -25,7 +25,6 @@
|
|||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="edit/lint.js"></script>
|
||||
<script src="edit/util.js"></script>
|
||||
<script src="edit/regexp-tester.js"></script>
|
||||
<script src="edit/applies-to-line-widget.js"></script>
|
||||
|
@ -65,6 +64,8 @@
|
|||
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||
|
||||
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet" />
|
||||
<script src="vendor/codemirror/addon/lint/lint.js"></script>
|
||||
|
||||
|
||||
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet" />
|
||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||
|
@ -87,6 +88,16 @@
|
|||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||
<script src="edit/codemirror-default.js"></script>
|
||||
|
||||
<script src="edit/linter.js"></script>
|
||||
<script src="edit/linter-defaults.js"></script>
|
||||
<script src="edit/linter-engines.js"></script>
|
||||
<script src="edit/linter-meta.js"></script>
|
||||
<script src="edit/linter-help-dialog.js"></script>
|
||||
<script src="edit/linter-report.js"></script>
|
||||
<script src="edit/linter-config-dialog.js"></script>
|
||||
|
||||
<script src="edit/editor-worker.js"></script>
|
||||
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<template data-id="appliesTo">
|
||||
|
@ -417,7 +428,7 @@
|
|||
</a>
|
||||
</h2>
|
||||
</summary>
|
||||
<div></div>
|
||||
<div class="lint-report-container"></div>
|
||||
</details>
|
||||
<div id="footer" class="hidden">
|
||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
global CodeMirror linterConfig loadScript
|
||||
global CodeMirror loadScript
|
||||
global editors editor styleId ownTabId
|
||||
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
|
||||
global getSectionsHashes
|
||||
|
@ -8,9 +8,6 @@ global messageBox
|
|||
'use strict';
|
||||
|
||||
onDOMscriptReady('/codemirror.js').then(() => {
|
||||
|
||||
CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
|
||||
|
||||
const COMMANDS = {
|
||||
save,
|
||||
toggleStyle,
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/* global importScripts parserlib CSSLint parseMozFormat */
|
||||
'use strict';
|
||||
|
||||
const CSSLINT_PATH = '/vendor-overwrites/csslint/';
|
||||
importScripts(CSSLINT_PATH + 'parserlib.js');
|
||||
|
||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
||||
|
||||
self.onmessage = ({data}) => {
|
||||
|
||||
const {action = 'run'} = data;
|
||||
|
||||
if (action === 'parse') {
|
||||
if (!self.parseMozFormat) self.importScripts('/js/moz-parser.js');
|
||||
self.postMessage(parseMozFormat(data));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.CSSLint) self.importScripts(CSSLINT_PATH + '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 {code, config} = data;
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -651,6 +651,9 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
|
|||
#lint table:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#lint table.empty {
|
||||
display: none;
|
||||
}
|
||||
#lint caption {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
|
|
31
edit/edit.js
31
edit/edit.js
|
@ -1,13 +1,12 @@
|
|||
/*
|
||||
global CodeMirror parserlib loadScript
|
||||
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
|
||||
global CodeMirror loadScript
|
||||
global createSourceEditor
|
||||
global closeCurrentTab regExpTester messageBox
|
||||
global setupCodeMirror
|
||||
global beautify
|
||||
global initWithSectionStyle addSections removeSection getSectionsHashes
|
||||
global sectionsToMozFormat
|
||||
global moveFocus
|
||||
global moveFocus editorWorker
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -212,7 +211,6 @@ function beforeUnload() {
|
|||
}
|
||||
const isDirty = editor ? editor.isDirty() : !isCleanGlobal();
|
||||
if (isDirty) {
|
||||
updateLintReportIfEnabled(null, 0);
|
||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||
return t('styleChangesNotSaved');
|
||||
}
|
||||
|
@ -276,9 +274,6 @@ function initHooks() {
|
|||
$('#save-button').addEventListener('click', save, false);
|
||||
$('#sections-help').addEventListener('click', showSectionHelp, false);
|
||||
|
||||
// TODO: investigate why FF needs this delay
|
||||
debounce(initLint, FIREFOX ? 100 : 0);
|
||||
|
||||
if (!FIREFOX) {
|
||||
$$([
|
||||
'input:not([type])',
|
||||
|
@ -353,7 +348,6 @@ function toggleStyle() {
|
|||
}
|
||||
|
||||
function save() {
|
||||
updateLintReportIfEnabled(null, 0);
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
|
@ -414,12 +408,6 @@ function updateTitle() {
|
|||
$('#save-button').disabled = clean;
|
||||
}
|
||||
|
||||
function updateLintReportIfEnabled(...args) {
|
||||
if (CodeMirror.defaults.lint) {
|
||||
updateLintReport(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function showMozillaFormat() {
|
||||
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
||||
popup.codebox.setValue(toMozillaFormat());
|
||||
|
@ -461,16 +449,7 @@ function fromMozillaFormat() {
|
|||
|
||||
function doImport({replaceOldStyle = false}) {
|
||||
lockPageUI(true);
|
||||
new Promise(setTimeout)
|
||||
.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(),
|
||||
}))
|
||||
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
|
||||
.then(({sections, errors}) => {
|
||||
// shouldn't happen but just in case
|
||||
if (!sections.length && errors.length) {
|
||||
|
@ -483,8 +462,7 @@ function fromMozillaFormat() {
|
|||
removeOldSections(replaceOldStyle);
|
||||
return addSections(sections, div => setCleanItem(div, false));
|
||||
})
|
||||
.then(sectionDivs => {
|
||||
sectionDivs.forEach(div => updateLintReportIfEnabled(div.CodeMirror, 1));
|
||||
.then(() => {
|
||||
$('.dismiss').dispatchEvent(new Event('click'));
|
||||
})
|
||||
.catch(showError)
|
||||
|
@ -604,7 +582,6 @@ function showCodeMirrorPopup(title, html, options) {
|
|||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
matchBrackets: true,
|
||||
lint: linterConfig.getForCodeMirror(),
|
||||
styleActiveLine: true,
|
||||
theme: prefs.get('editor.theme'),
|
||||
keyMap: prefs.get('editor.keyMap')
|
||||
|
|
118
edit/editor-worker-body.js
Normal file
118
edit/editor-worker-body.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
/* global importScripts parseMozFormat parserlib CSSLint require */
|
||||
'use strict';
|
||||
|
||||
createAPI({
|
||||
csslint: (code, config) => {
|
||||
loadParserLib();
|
||||
loadScript(['/vendor-overwrites/csslint/csslint.js']);
|
||||
return CSSLint.verify(code, config).messages
|
||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||
},
|
||||
stylelint: (code, config) => {
|
||||
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
|
||||
return require('stylelint').lint({code, config});
|
||||
},
|
||||
parseMozFormat: data => {
|
||||
loadParserLib();
|
||||
loadScript(['/js/moz-parser.js']);
|
||||
return parseMozFormat(data);
|
||||
},
|
||||
getStylelintRules,
|
||||
getCsslintRules
|
||||
});
|
||||
|
||||
function getCsslintRules() {
|
||||
loadScript(['/vendor-overwrites/csslint/csslint.js']);
|
||||
return CSSLint.getRules().map(rule => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (typeof value !== 'function') {
|
||||
output[key] = value;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
function getStylelintRules() {
|
||||
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
|
||||
const stylelint = require('stylelint');
|
||||
const options = {};
|
||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||
for (const id of Object.keys(stylelint.rules)) {
|
||||
const ruleCode = String(stylelint.rules[id]);
|
||||
const sets = [];
|
||||
let m, mStr;
|
||||
while ((m = rxPossible.exec(ruleCode))) {
|
||||
const possible = m[1];
|
||||
const set = [];
|
||||
while ((mStr = rxString.exec(possible))) {
|
||||
const s = mStr[1];
|
||||
if (s.includes(' ')) {
|
||||
set.push(...s.split(/\s+/));
|
||||
} else {
|
||||
set.push(s);
|
||||
}
|
||||
}
|
||||
if (possible.includes('ignoreAtRules')) {
|
||||
set.push('ignoreAtRules');
|
||||
}
|
||||
if (possible.includes('ignoreShorthands')) {
|
||||
set.push('ignoreShorthands');
|
||||
}
|
||||
if (set.length) {
|
||||
sets.push(set);
|
||||
}
|
||||
}
|
||||
if (sets.length) {
|
||||
options[id] = sets;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function loadParserLib() {
|
||||
if (typeof parserlib !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
importScripts('/vendor-overwrites/csslint/parserlib.js');
|
||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
||||
}
|
||||
|
||||
const loadedUrls = new Set();
|
||||
function loadScript(urls) {
|
||||
urls = urls.filter(u => !loadedUrls.has(u));
|
||||
importScripts(...urls);
|
||||
urls.forEach(u => loadedUrls.add(u));
|
||||
}
|
||||
|
||||
function createAPI(methods) {
|
||||
self.onmessage = e => {
|
||||
const message = e.data;
|
||||
Promise.resolve()
|
||||
.then(() => methods[message.action](...message.args))
|
||||
.then(result => ({
|
||||
id: message.id,
|
||||
error: false,
|
||||
data: result
|
||||
}))
|
||||
.catch(err => ({
|
||||
id: message.id,
|
||||
error: true,
|
||||
data: cloneError(err)
|
||||
}))
|
||||
.then(data => self.postMessage(data));
|
||||
};
|
||||
}
|
||||
|
||||
function cloneError(err) {
|
||||
return Object.assign({
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
message: err.message,
|
||||
lineNumber: err.lineNumber,
|
||||
columnNumber: err.columnNumber,
|
||||
fileName: err.fileName
|
||||
}, err);
|
||||
}
|
39
edit/editor-worker.js
Normal file
39
edit/editor-worker.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var editorWorker = (() => {
|
||||
let worker;
|
||||
return new Proxy({}, {
|
||||
get: (target, prop) =>
|
||||
(...args) => {
|
||||
if (!worker) {
|
||||
worker = createWorker();
|
||||
}
|
||||
return worker.invoke(prop, args);
|
||||
}
|
||||
});
|
||||
|
||||
function createWorker() {
|
||||
let id = 0;
|
||||
const pendingResponse = new Map();
|
||||
const worker = new Worker('/edit/editor-worker-body.js');
|
||||
worker.onmessage = e => {
|
||||
const message = e.data;
|
||||
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
|
||||
pendingResponse.delete(message.id);
|
||||
};
|
||||
return {invoke};
|
||||
|
||||
function invoke(action, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingResponse.set(id, {resolve, reject});
|
||||
worker.postMessage({
|
||||
id,
|
||||
action,
|
||||
args
|
||||
});
|
||||
id++;
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -1,43 +0,0 @@
|
|||
/* global CodeMirror linterConfig */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
CodeMirror.registerHelper('lint', 'csslint', invokeHelper);
|
||||
CodeMirror.registerHelper('lint', 'stylelint', invokeHelper);
|
||||
|
||||
const COOKS = {
|
||||
csslint: results =>
|
||||
results.map(({line, col: ch, message, rule, type: severity}) => line && {
|
||||
message,
|
||||
from: {line: line - 1, ch: ch - 1},
|
||||
to: {line: line - 1, ch},
|
||||
rule: rule.id,
|
||||
severity,
|
||||
}).filter(Boolean),
|
||||
|
||||
stylelint({results}, cm) {
|
||||
if (!results[0]) return [];
|
||||
const output = results[0].warnings.map(({line, column: ch, text, severity}) => ({
|
||||
from: {line: line - 1, ch: ch - 1},
|
||||
to: {line: line - 1, ch},
|
||||
message: text
|
||||
.replace('Unexpected ', '')
|
||||
.replace(/^./, firstLetter => firstLetter.toUpperCase())
|
||||
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
|
||||
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
|
||||
severity,
|
||||
}));
|
||||
return cm.doc.mode.name !== 'stylus' ?
|
||||
output :
|
||||
output.filter(({message}) =>
|
||||
!message.includes('"@css"') || !message.includes('(at-rule-no-unknown)'));
|
||||
},
|
||||
};
|
||||
|
||||
function invokeHelper(code, options, cm) {
|
||||
const config = linterConfig.getCurrent();
|
||||
const cook = COOKS[linterConfig.getName()];
|
||||
return linterConfig.invokeWorker({code, config})
|
||||
.then(data => cook(data, cm));
|
||||
}
|
||||
})();
|
|
@ -1,50 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* CSSLint Config values
|
||||
* 0 = disabled; 1 = warning; 2 = error
|
||||
*/
|
||||
window.linterConfig.defaults.csslint = {
|
||||
// Default warnings
|
||||
'display-property-grouping': 1,
|
||||
'duplicate-properties': 1,
|
||||
'empty-rules': 1,
|
||||
'errors': 1,
|
||||
'warnings': 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
|
||||
};
|
|
@ -1,170 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
window.linterConfig.defaults.stylelint = (defaultSeverity => ({
|
||||
// 'sugarss' is a indent-based syntax like Sass or Stylus
|
||||
// ref: https://github.com/postcss/postcss#syntaxes
|
||||
syntax: 'sugarss',
|
||||
// ** recommended rules **
|
||||
// ref: https://github.com/stylelint/stylelint-config-recommended/blob/master/index.js
|
||||
rules: {
|
||||
'at-rule-no-unknown': [true, defaultSeverity],
|
||||
'block-no-empty': [true, defaultSeverity],
|
||||
'color-no-invalid-hex': [true, defaultSeverity],
|
||||
'declaration-block-no-duplicate-properties': [true, {
|
||||
'ignore': ['consecutive-duplicates-with-different-values'],
|
||||
'severity': 'warning'
|
||||
}],
|
||||
'declaration-block-no-shorthand-property-overrides': [true, defaultSeverity],
|
||||
'font-family-no-duplicate-names': [true, defaultSeverity],
|
||||
'function-calc-no-unspaced-operator': [true, defaultSeverity],
|
||||
'function-linear-gradient-no-nonstandard-direction': [true, defaultSeverity],
|
||||
'keyframe-declaration-no-important': [true, defaultSeverity],
|
||||
'media-feature-name-no-unknown': [true, defaultSeverity],
|
||||
/* recommended true */
|
||||
'no-empty-source': false,
|
||||
'no-extra-semicolons': [true, defaultSeverity],
|
||||
'no-invalid-double-slash-comments': [true, defaultSeverity],
|
||||
'property-no-unknown': [true, defaultSeverity],
|
||||
'selector-pseudo-class-no-unknown': [true, defaultSeverity],
|
||||
'selector-pseudo-element-no-unknown': [true, defaultSeverity],
|
||||
'selector-type-no-unknown': false, // for scss/less/stylus-lang
|
||||
'string-no-newline': [true, defaultSeverity],
|
||||
'unit-no-unknown': [true, defaultSeverity],
|
||||
|
||||
// ** non-essential rules
|
||||
'comment-no-empty': false,
|
||||
'declaration-block-no-redundant-longhand-properties': false,
|
||||
'shorthand-property-no-redundant-values': false,
|
||||
|
||||
// ** stylistic rules **
|
||||
/*
|
||||
'at-rule-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'blockless-after-same-name-blockless',
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment'
|
||||
]
|
||||
}
|
||||
],
|
||||
'at-rule-name-case': 'lower',
|
||||
'at-rule-name-space-after': 'always-single-line',
|
||||
'at-rule-semicolon-newline-after': 'always',
|
||||
'block-closing-brace-empty-line-before': 'never',
|
||||
'block-closing-brace-newline-after': 'always',
|
||||
'block-closing-brace-newline-before': 'always-multi-line',
|
||||
'block-closing-brace-space-before': 'always-single-line',
|
||||
'block-opening-brace-newline-after': 'always-multi-line',
|
||||
'block-opening-brace-space-after': 'always-single-line',
|
||||
'block-opening-brace-space-before': 'always',
|
||||
'color-hex-case': 'lower',
|
||||
'color-hex-length': 'short',
|
||||
'comment-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'stylelint-commands'
|
||||
]
|
||||
}
|
||||
],
|
||||
'comment-whitespace-inside': 'always',
|
||||
'custom-property-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'after-custom-property',
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment',
|
||||
'inside-single-line-block'
|
||||
]
|
||||
}
|
||||
],
|
||||
'declaration-bang-space-after': 'never',
|
||||
'declaration-bang-space-before': 'always',
|
||||
'declaration-block-semicolon-newline-after': 'always-multi-line',
|
||||
'declaration-block-semicolon-space-after': 'always-single-line',
|
||||
'declaration-block-semicolon-space-before': 'never',
|
||||
'declaration-block-single-line-max-declarations': 1,
|
||||
'declaration-block-trailing-semicolon': 'always',
|
||||
'declaration-colon-newline-after': 'always-multi-line',
|
||||
'declaration-colon-space-after': 'always-single-line',
|
||||
'declaration-colon-space-before': 'never',
|
||||
'declaration-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'after-declaration',
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment',
|
||||
'inside-single-line-block'
|
||||
]
|
||||
}
|
||||
],
|
||||
'function-comma-newline-after': 'always-multi-line',
|
||||
'function-comma-space-after': 'always-single-line',
|
||||
'function-comma-space-before': 'never',
|
||||
'function-max-empty-lines': 0,
|
||||
'function-name-case': 'lower',
|
||||
'function-parentheses-newline-inside': 'always-multi-line',
|
||||
'function-parentheses-space-inside': 'never-single-line',
|
||||
'function-whitespace-after': 'always',
|
||||
'indentation': 2,
|
||||
'length-zero-no-unit': true,
|
||||
'max-empty-lines': 1,
|
||||
'media-feature-colon-space-after': 'always',
|
||||
'media-feature-colon-space-before': 'never',
|
||||
'media-feature-name-case': 'lower',
|
||||
'media-feature-parentheses-space-inside': 'never',
|
||||
'media-feature-range-operator-space-after': 'always',
|
||||
'media-feature-range-operator-space-before': 'always',
|
||||
'media-query-list-comma-newline-after': 'always-multi-line',
|
||||
'media-query-list-comma-space-after': 'always-single-line',
|
||||
'media-query-list-comma-space-before': 'never',
|
||||
'no-eol-whitespace': true,
|
||||
'no-missing-end-of-source-newline': true,
|
||||
'number-leading-zero': 'always',
|
||||
'number-no-trailing-zeros': true,
|
||||
'property-case': 'lower',
|
||||
'rule-empty-line-before': [
|
||||
'always-multi-line',
|
||||
{
|
||||
'except': [
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment'
|
||||
]
|
||||
}
|
||||
],
|
||||
'selector-attribute-brackets-space-inside': 'never',
|
||||
'selector-attribute-operator-space-after': 'never',
|
||||
'selector-attribute-operator-space-before': 'never',
|
||||
'selector-combinator-space-after': 'always',
|
||||
'selector-combinator-space-before': 'always',
|
||||
'selector-descendant-combinator-no-non-space': true,
|
||||
'selector-list-comma-newline-after': 'always',
|
||||
'selector-list-comma-space-before': 'never',
|
||||
'selector-max-empty-lines': 0,
|
||||
'selector-pseudo-class-case': 'lower',
|
||||
'selector-pseudo-class-parentheses-space-inside': 'never',
|
||||
'selector-pseudo-element-case': 'lower',
|
||||
'selector-pseudo-element-colon-notation': 'double',
|
||||
'selector-type-case': 'lower',
|
||||
'unit-case': 'lower',
|
||||
'value-list-comma-newline-after': 'always-multi-line',
|
||||
'value-list-comma-space-after': 'always-single-line',
|
||||
'value-list-comma-space-before': 'never',
|
||||
'value-list-max-empty-lines': 0
|
||||
*/
|
||||
}
|
||||
}))({severity: 'warning'});
|
558
edit/lint.js
558
edit/lint.js
|
@ -1,558 +0,0 @@
|
|||
/* global CodeMirror messageBox */
|
||||
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
|
||||
/* global loadScript require CSSLint stylelint */
|
||||
/* global clipString */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(loadLinterAssets);
|
||||
|
||||
// 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',
|
||||
},
|
||||
worker: {
|
||||
csslint: {path: '/edit/csslint-loader.js'},
|
||||
stylelint: {path: '/edit/stylelint-loader.js'},
|
||||
},
|
||||
allRuleIds: {
|
||||
csslint: null,
|
||||
stylelint: null,
|
||||
},
|
||||
|
||||
getName() {
|
||||
// some dirty hacks to override editor.linter getting from prefs
|
||||
const linter = prefs.get('editor.linter');
|
||||
const mode = linter && editors[0] && editors[0].doc.mode;
|
||||
return mode && ((mode.name || mode) !== 'css' || mode.helperType) ? 'stylelint' : linter;
|
||||
},
|
||||
|
||||
getCurrent(linter = linterConfig.getName()) {
|
||||
return this.fallbackToDefaults(this[linter] || {});
|
||||
},
|
||||
|
||||
getForCodeMirror(linter = linterConfig.getName()) {
|
||||
return CodeMirror.lint && CodeMirror.lint[linter] ? {
|
||||
getAnnotations: CodeMirror.lint[linter],
|
||||
delay: prefs.get('editor.lintDelay'),
|
||||
onUpdateLinting(annotationsNotSorted, annotations, cm) {
|
||||
updateLintReport(cm);
|
||||
},
|
||||
} : false;
|
||||
},
|
||||
|
||||
fallbackToDefaults(config, linter = linterConfig.getName()) {
|
||||
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 Object.assign({}, this.defaults[linter] || {}, config);
|
||||
} else {
|
||||
return deepCopy(this.defaults[linter] || {});
|
||||
}
|
||||
},
|
||||
|
||||
setLinter(linter = linterConfig.getName()) {
|
||||
linter = linter.toLowerCase();
|
||||
linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
|
||||
if (linterConfig.getName() !== linter) {
|
||||
prefs.set('editor.linter', linter);
|
||||
}
|
||||
return linter;
|
||||
},
|
||||
|
||||
invokeWorker(message) {
|
||||
const worker = linterConfig.worker[message.linter || linterConfig.getName()];
|
||||
if (!worker.queue) {
|
||||
worker.queue = [];
|
||||
worker.instance.onmessage = ({data}) => {
|
||||
worker.queue.shift().resolve(data);
|
||||
if (worker.queue.length) {
|
||||
worker.instance.postMessage(worker.queue[0].message);
|
||||
}
|
||||
};
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
worker.queue.push({message, resolve});
|
||||
if (worker.queue.length === 1) {
|
||||
worker.instance.postMessage(message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getAllRuleIds(linter = linterConfig.getName()) {
|
||||
return Promise.resolve(
|
||||
this.allRuleIds[linter] ||
|
||||
this.invokeWorker({linter, action: 'getAllRuleIds'})
|
||||
.then(ids => (this.allRuleIds[linter] = ids.sort()))
|
||||
);
|
||||
},
|
||||
|
||||
findInvalidRules(config, linter = linterConfig.getName()) {
|
||||
return this.getAllRuleIds(linter).then(allRuleIds => {
|
||||
const allRuleIdsSet = new Set(allRuleIds);
|
||||
const rules = linter === 'stylelint' ? config.rules : config;
|
||||
return Object.keys(rules).filter(rule => !allRuleIdsSet.has(rule));
|
||||
});
|
||||
},
|
||||
|
||||
stringify(config = this.getCurrent()) {
|
||||
if (linterConfig.getName() === '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 = linterConfig.getName();
|
||||
this[linter] = config;
|
||||
chromeSync.setLZValue(this.storageName[linter], config);
|
||||
return config;
|
||||
},
|
||||
|
||||
loadAll() {
|
||||
return 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(updateLinter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// this is an event listener so it can't refer to self via 'this'
|
||||
openOnClick(event) {
|
||||
event.preventDefault();
|
||||
setupLinterPopup(linterConfig.stringify());
|
||||
},
|
||||
|
||||
init() {
|
||||
if (!this.init.pending) this.init.pending = this.loadAll();
|
||||
return this.init.pending;
|
||||
}
|
||||
};
|
||||
|
||||
function initLint() {
|
||||
$('#lint-help').addEventListener('click', showLintHelp);
|
||||
$('#lint').addEventListener('click', gotoLintIssue);
|
||||
$('#linter-settings').addEventListener('click', linterConfig.openOnClick);
|
||||
|
||||
updateLinter();
|
||||
linterConfig.watchStorage();
|
||||
prefs.subscribe(['editor.linter'], updateLinter);
|
||||
}
|
||||
|
||||
function updateLinter({immediately, linter = linterConfig.getName()} = {}) {
|
||||
if (!immediately) {
|
||||
debounce(updateLinter, 0, {immediately: true, linter});
|
||||
return;
|
||||
}
|
||||
const GUTTERS_CLASS = 'CodeMirror-lint-markers';
|
||||
|
||||
Promise.all([
|
||||
linterConfig.init(),
|
||||
loadLinterAssets(linter)
|
||||
]).then(updateEditors);
|
||||
$('#linter-settings').style.display = !linter ? 'none' : 'inline-block';
|
||||
$('#lint').classList.add('hidden');
|
||||
|
||||
function updateEditors() {
|
||||
CodeMirror.defaults.lint = linterConfig.getForCodeMirror(linter);
|
||||
const guttersOption = prepareGuttersOption();
|
||||
editors.forEach(cm => {
|
||||
if (cm.options.lint !== CodeMirror.defaults.lint) {
|
||||
cm.setOption('lint', CodeMirror.defaults.lint);
|
||||
}
|
||||
if (guttersOption) {
|
||||
cm.setOption('guttersOption', guttersOption);
|
||||
updateGutters(cm, guttersOption);
|
||||
cm.refresh();
|
||||
}
|
||||
setTimeout(updateLintReport, 0, cm);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareGuttersOption() {
|
||||
const gutters = CodeMirror.defaults.gutters;
|
||||
const needRefresh = Boolean(linter) !== gutters.includes(GUTTERS_CLASS);
|
||||
if (needRefresh) {
|
||||
if (linter) {
|
||||
gutters.push(GUTTERS_CLASS);
|
||||
} else {
|
||||
gutters.splice(gutters.indexOf(GUTTERS_CLASS), 1);
|
||||
}
|
||||
}
|
||||
return needRefresh && gutters;
|
||||
}
|
||||
|
||||
function updateGutters(cm, guttersOption) {
|
||||
cm.options.gutters = guttersOption;
|
||||
const el = $('.' + GUTTERS_CLASS, cm.display.gutters);
|
||||
if (linter && !el) {
|
||||
cm.display.gutters.appendChild($create('.CodeMirror-gutter ' + GUTTERS_CLASS));
|
||||
} else if (!linter && el) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLintReport(cm, delay) {
|
||||
const state = cm && cm.state && cm.state.lint || {};
|
||||
if (delay === 0) {
|
||||
// immediately show pending csslint/stylelint messages in onbeforeunload and save
|
||||
clearTimeout(state.lintTimeout);
|
||||
updateLintReportInternal(cm);
|
||||
return;
|
||||
}
|
||||
if (delay > 0) {
|
||||
clearTimeout(state.lintTimeout);
|
||||
state.lintTimeout = setTimeout(cm => {
|
||||
if (cm.performLint) {
|
||||
cm.performLint();
|
||||
updateLintReportInternal(cm);
|
||||
}
|
||||
}, delay, cm);
|
||||
return;
|
||||
}
|
||||
if (state.options) {
|
||||
clearTimeout(state.reportTimeout);
|
||||
const delay = cm && cm.state.renderLintReportNow ? 0 : state.options.delay + 100;
|
||||
state.reportTimeout = setTimeout(updateLintReportInternal, delay, cm, {
|
||||
postponeNewIssues: delay === undefined || delay === null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateLintReportInternal(scope, {postponeNewIssues} = {}) {
|
||||
const {changed, fixedSome} = (scope ? [scope] : editors).reduce(process, {});
|
||||
if (changed) {
|
||||
const renderNow = editors.last.state.renderLintReportNow =
|
||||
!postponeNewIssues || fixedSome || editors.last.state.renderLintReportNow;
|
||||
debounce(renderLintReport, renderNow ? 0 : CodeMirror.defaults.lintReportDelay, true);
|
||||
}
|
||||
|
||||
function process(result, cm) {
|
||||
const lintState = cm.state.lint || {};
|
||||
const oldMarkers = lintState.stylusMarkers || new Map();
|
||||
const newMarkers = lintState.stylusMarkers = new Map();
|
||||
const oldText = (lintState.body || {}).textContentCached || '';
|
||||
const activeLine = cm.getCursor().line;
|
||||
const body = !(lintState.marked || {}).length ? {} :
|
||||
$create('tbody', lintState.marked.map(mark => {
|
||||
const info = mark.__annotation;
|
||||
const {line, ch} = info.from;
|
||||
const isActiveLine = line === activeLine;
|
||||
const pos = isActiveLine ? 'cursor' : (line + ',' + ch);
|
||||
const title = clipString(info.message, 1000) + `\n(${info.rule})`;
|
||||
const message = clipString(info.message, 100);
|
||||
if (isActiveLine || oldMarkers[pos] === message) {
|
||||
oldMarkers.delete(pos);
|
||||
}
|
||||
newMarkers.set(pos, message);
|
||||
return $create(`tr.${info.severity}`, [
|
||||
$create('td', {attributes: {role: 'severity'}, dataset: {rule: info.rule}},
|
||||
$create('.CodeMirror-lint-marker-' + info.severity, info.severity)),
|
||||
$create('td', {attributes: {role: 'line'}}, line + 1),
|
||||
$create('td', {attributes: {role: 'sep'}}, ':'),
|
||||
$create('td', {attributes: {role: 'col'}}, ch + 1),
|
||||
$create('td', {attributes: {role: 'message'}, title}, message),
|
||||
]);
|
||||
}));
|
||||
body.textContentCached = body.textContent || '';
|
||||
lintState.body = body.textContentCached && body;
|
||||
result.changed |= oldText !== body.textContentCached;
|
||||
result.fixedSome |= lintState.reportDisplayed && oldMarkers.size;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLintReport(someBlockChanged) {
|
||||
const container = $('#lint');
|
||||
const content = container.children[1];
|
||||
const label = t('sectionCode');
|
||||
const newContent = content.cloneNode(false);
|
||||
let issueCount = 0;
|
||||
editors.forEach((cm, index) => {
|
||||
cm.state.renderLintReportNow = false;
|
||||
const lintState = cm.state.lint || {};
|
||||
const body = lintState.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const newBlock = $create('table', {cm}, [
|
||||
$create('caption', label + ' ' + (index + 1)),
|
||||
body,
|
||||
]);
|
||||
newContent.appendChild(newBlock);
|
||||
issueCount += newBlock.rows.length;
|
||||
|
||||
const block = content.children[newContent.children.length - 1];
|
||||
const blockChanged =
|
||||
!block ||
|
||||
block.cm !== cm ||
|
||||
body.textContentCached !== block.textContentCached;
|
||||
someBlockChanged |= blockChanged;
|
||||
lintState.reportDisplayed = blockChanged;
|
||||
});
|
||||
if (someBlockChanged || newContent.children.length !== content.children.length) {
|
||||
$('#issue-count').textContent = issueCount;
|
||||
container.replaceChild(newContent, content);
|
||||
container.classList.toggle('hidden', !newContent.children.length);
|
||||
}
|
||||
}
|
||||
|
||||
function gotoLintIssue(event) {
|
||||
const issue = event.target.closest('tr');
|
||||
if (!issue) {
|
||||
return;
|
||||
}
|
||||
const block = issue.closest('table');
|
||||
makeSectionVisible(block.cm);
|
||||
block.cm.focus();
|
||||
block.cm.setSelection({
|
||||
line: parseInt($('td[role="line"]', issue).textContent) - 1,
|
||||
ch: parseInt($('td[role="col"]', issue).textContent) - 1
|
||||
});
|
||||
}
|
||||
|
||||
function showLintHelp() {
|
||||
const linter = linterConfig.getName();
|
||||
const baseUrl = linter === 'stylelint'
|
||||
? 'https://stylelint.io/user-guide/rules/'
|
||||
// some CSSLint rules do not have a url
|
||||
: 'https://github.com/CSSLint/csslint/issues/535';
|
||||
let headerLink, template, csslintRules;
|
||||
if (linter === 'csslint') {
|
||||
headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules', 'CSSLint');
|
||||
template = ruleID => {
|
||||
const rule = csslintRules.find(rule => rule.id === ruleID);
|
||||
return rule &&
|
||||
$create('li', [
|
||||
$create('b', $createLink(rule.url || baseUrl, rule.name)),
|
||||
$create('br'),
|
||||
rule.desc,
|
||||
]);
|
||||
};
|
||||
} else {
|
||||
headerLink = $createLink(baseUrl, 'stylelint');
|
||||
template = rule =>
|
||||
$create('li',
|
||||
rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
|
||||
}
|
||||
const header = t('linterIssuesHelp', '\x01').split('\x01');
|
||||
const activeRules = new Set($$('#lint td[role="severity"]').map(el => el.dataset.rule));
|
||||
Promise.resolve(linter !== 'csslint' || linterConfig.invokeWorker({action: 'getAllRuleInfos'}))
|
||||
.then(data => {
|
||||
csslintRules = data;
|
||||
showHelp(t('linterIssues'),
|
||||
$create([
|
||||
header[0], headerLink, header[1],
|
||||
$create('ul.rules', [...activeRules.values()].map(template)),
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function showLinterErrorMessage(title, contents, popup) {
|
||||
messageBox({
|
||||
title,
|
||||
contents,
|
||||
className: 'danger center lint-config',
|
||||
buttons: [t('confirmOK')],
|
||||
}).then(() => popup && popup.codebox && popup.codebox.focus());
|
||||
}
|
||||
|
||||
function setupLinterPopup(config) {
|
||||
const linter = linterConfig.getName();
|
||||
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
||||
const defaultConfig = linterConfig.stringify(linterConfig.defaults[linter] || {});
|
||||
const title = t('linterConfigPopupTitle', linterTitle);
|
||||
const popup = showCodeMirrorPopup(title, null, {
|
||||
lint: false,
|
||||
extraKeys: {'Ctrl-Enter': save},
|
||||
hintOptions: {hint},
|
||||
});
|
||||
$('.contents', popup).appendChild(makeFooter());
|
||||
|
||||
let cm = popup.codebox;
|
||||
cm.focus();
|
||||
cm.setValue(config);
|
||||
cm.clearHistory();
|
||||
cm.markClean();
|
||||
cm.on('changes', updateButtonState);
|
||||
updateButtonState();
|
||||
|
||||
cm.rerouteHotkeys(false);
|
||||
window.addEventListener('closeHelp', function _() {
|
||||
window.removeEventListener('closeHelp', _);
|
||||
cm.rerouteHotkeys(true);
|
||||
cm = null;
|
||||
});
|
||||
|
||||
loadScript([
|
||||
'/vendor/codemirror/mode/javascript/javascript.js',
|
||||
'/vendor/codemirror/addon/lint/json-lint.js',
|
||||
'/vendor/jsonlint/jsonlint.js'
|
||||
]).then(() => {
|
||||
cm.setOption('mode', 'application/json');
|
||||
cm.setOption('lint', 'json');
|
||||
});
|
||||
|
||||
function makeFooter() {
|
||||
return $create('div', [
|
||||
$create('p', [
|
||||
$createLink(
|
||||
linter === 'stylelint'
|
||||
? 'https://stylelint.io/user-guide/rules/'
|
||||
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
|
||||
t('linterRulesLink')),
|
||||
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '',
|
||||
]),
|
||||
$create('.buttons', [
|
||||
$create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')),
|
||||
$create('button.cancel', {onclick: cancel}, t('confirmClose')),
|
||||
$create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function save(event) {
|
||||
if (event instanceof Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
const json = tryJSONparse(cm.getValue());
|
||||
if (!json) {
|
||||
showLinterErrorMessage(linter, t('linterJSONError'), popup);
|
||||
cm.focus();
|
||||
return;
|
||||
}
|
||||
linterConfig.findInvalidRules(json, linter).then(invalid => {
|
||||
if (invalid.length) {
|
||||
showLinterErrorMessage(linter, [
|
||||
t('linterInvalidConfigError'),
|
||||
$create('ul', invalid.map(name => $create('li', name))),
|
||||
], popup);
|
||||
return;
|
||||
}
|
||||
linterConfig.setLinter(linter);
|
||||
linterConfig.save(json);
|
||||
cm.markClean();
|
||||
cm.focus();
|
||||
updateButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
function reset(event) {
|
||||
event.preventDefault();
|
||||
linterConfig.setLinter(linter);
|
||||
cm.setValue(defaultConfig);
|
||||
cm.focus();
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
function cancel(event) {
|
||||
event.preventDefault();
|
||||
$('.dismiss').dispatchEvent(new Event('click'));
|
||||
}
|
||||
|
||||
function updateButtonState() {
|
||||
$('.save', popup).disabled = cm.isClean();
|
||||
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
|
||||
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
|
||||
}
|
||||
|
||||
function hint(cm) {
|
||||
return Promise.all([
|
||||
linterConfig.getAllRuleIds(linter),
|
||||
linter !== 'stylelint' || hint.allOptions ||
|
||||
linterConfig.invokeWorker({action: 'getAllRuleOptions', linter})
|
||||
.then(options => (hint.allOptions = options)),
|
||||
])
|
||||
.then(([ruleIds, options]) => {
|
||||
const cursor = cm.getCursor();
|
||||
const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
|
||||
const {line, ch} = cursor;
|
||||
|
||||
const quoted = string.startsWith('"');
|
||||
const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
|
||||
const depth = getLexicalDepth(lexical);
|
||||
|
||||
const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
|
||||
let [, prevWord] = search.find(true) || [];
|
||||
let words = [];
|
||||
|
||||
if (depth === 1 && linter === 'stylelint') {
|
||||
words = quoted ? ['rules'] : [];
|
||||
} else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
|
||||
words = ruleIds;
|
||||
} else if (depth === 2 || depth === 3 && lexical.type === ']') {
|
||||
words = !quoted ? ['true', 'false', 'null'] :
|
||||
ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
|
||||
} else if (depth === 4 && prevWord === 'severity') {
|
||||
words = ['error', 'warning'];
|
||||
} else if (depth === 4) {
|
||||
words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
|
||||
} else if (depth === 5 && lexical.type === ']' && quoted) {
|
||||
while (prevWord && !ruleIds.includes(prevWord)) {
|
||||
prevWord = (search.find(true) || [])[1];
|
||||
}
|
||||
words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
|
||||
}
|
||||
return {
|
||||
list: words.filter(word => word.startsWith(leftPart)),
|
||||
from: {line, ch: start + (quoted ? 1 : 0)},
|
||||
to: {line, ch: string.endsWith('"') ? end - 1 : end},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getLexicalDepth(lexicalState) {
|
||||
let depth = 0;
|
||||
while ((lexicalState = lexicalState.prev)) {
|
||||
depth++;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
|
||||
function loadLinterAssets(name = linterConfig.getName()) {
|
||||
const worker = linterConfig.worker[name];
|
||||
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',
|
||||
'/vendor/codemirror/addon/lint/lint.js',
|
||||
'/edit/lint-codemirror-helper.js');
|
||||
}
|
||||
return scripts.length ? loadScript(scripts) : Promise.resolve();
|
||||
}
|
196
edit/linter-config-dialog.js
Normal file
196
edit/linter-config-dialog.js
Normal file
|
@ -0,0 +1,196 @@
|
|||
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
$('#linter-settings').addEventListener('click', showLintConfig);
|
||||
}, {once: true});
|
||||
|
||||
function stringifyConfig(config) {
|
||||
return JSON.stringify(config, null, 2)
|
||||
.replace(/,\n\s+\{\n\s+("severity":\s"\w+")\n\s+\}/g, ', {$1}');
|
||||
}
|
||||
|
||||
function showLinterErrorMessage(title, contents, popup) {
|
||||
messageBox({
|
||||
title,
|
||||
contents,
|
||||
className: 'danger center lint-config',
|
||||
buttons: [t('confirmOK')],
|
||||
}).then(() => popup && popup.codebox && popup.codebox.focus());
|
||||
}
|
||||
|
||||
function showLintConfig() {
|
||||
const linter = $('#editor.linter').value;
|
||||
if (!linter) {
|
||||
return;
|
||||
}
|
||||
const storageName = linter === 'styleint' ? 'editorStylelintConfig' : 'editorCSSLintConfig';
|
||||
const getRules = memoize(linter === 'stylelint' ?
|
||||
editorWorker.getStylelintRules : editorWorker.getCsslintRules);
|
||||
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
||||
const defaultConfig = stringifyConfig(
|
||||
linter === 'stylelint' ? LINTER_DEFAULTS.STYLELINT : LINTER_DEFAULTS.CSSLINT
|
||||
);
|
||||
const title = t('linterConfigPopupTitle', linterTitle);
|
||||
const popup = showCodeMirrorPopup(title, null, {
|
||||
lint: false,
|
||||
extraKeys: {'Ctrl-Enter': save},
|
||||
hintOptions: {hint},
|
||||
});
|
||||
$('.contents', popup).appendChild(makeFooter());
|
||||
|
||||
let cm = popup.codebox;
|
||||
cm.focus();
|
||||
chromeSync.getLZValue(storageName).then(config => {
|
||||
cm.setValue(config ? stringifyConfig(config) : defaultConfig);
|
||||
cm.clearHistory();
|
||||
cm.markClean();
|
||||
updateButtonState();
|
||||
});
|
||||
cm.on('changes', updateButtonState);
|
||||
|
||||
cm.rerouteHotkeys(false);
|
||||
window.addEventListener('closeHelp', function _() {
|
||||
window.removeEventListener('closeHelp', _);
|
||||
cm.rerouteHotkeys(true);
|
||||
cm = null;
|
||||
});
|
||||
|
||||
loadScript([
|
||||
'/vendor/codemirror/mode/javascript/javascript.js',
|
||||
'/vendor/codemirror/addon/lint/json-lint.js',
|
||||
'/vendor/jsonlint/jsonlint.js'
|
||||
]).then(() => {
|
||||
cm.setOption('mode', 'application/json');
|
||||
cm.setOption('lint', true);
|
||||
});
|
||||
|
||||
function findInvalidRules(config, linter) {
|
||||
return getRules()
|
||||
.then(rules => {
|
||||
if (linter === 'stylelint') {
|
||||
return Object.keys(config.rules).filter(k => !rules.hasOwnProperty(k));
|
||||
}
|
||||
const ruleSet = new Set(rules.map(r => r.id));
|
||||
return Object.keys(config).filter(k => !ruleSet.has(k));
|
||||
});
|
||||
}
|
||||
|
||||
function makeFooter() {
|
||||
return $create('div', [
|
||||
$create('p', [
|
||||
$createLink(
|
||||
linter === 'stylelint'
|
||||
? 'https://stylelint.io/user-guide/rules/'
|
||||
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
|
||||
t('linterRulesLink')),
|
||||
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '',
|
||||
]),
|
||||
$create('.buttons', [
|
||||
$create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')),
|
||||
$create('button.cancel', {onclick: cancel}, t('confirmClose')),
|
||||
$create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function save(event) {
|
||||
if (event instanceof Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
const json = tryJSONparse(cm.getValue());
|
||||
if (!json) {
|
||||
showLinterErrorMessage(linter, t('linterJSONError'), popup);
|
||||
cm.focus();
|
||||
return;
|
||||
}
|
||||
findInvalidRules(json, linter).then(invalid => {
|
||||
if (invalid.length) {
|
||||
showLinterErrorMessage(linter, [
|
||||
t('linterInvalidConfigError'),
|
||||
$create('ul', invalid.map(name => $create('li', name))),
|
||||
], popup);
|
||||
return;
|
||||
}
|
||||
chromeSync.setLZValue(storageName, json);
|
||||
cm.markClean();
|
||||
cm.focus();
|
||||
updateButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
function reset(event) {
|
||||
event.preventDefault();
|
||||
cm.setValue(defaultConfig);
|
||||
cm.focus();
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
function cancel(event) {
|
||||
event.preventDefault();
|
||||
$('.dismiss').dispatchEvent(new Event('click'));
|
||||
}
|
||||
|
||||
function updateButtonState() {
|
||||
$('.save', popup).disabled = cm.isClean();
|
||||
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
|
||||
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
|
||||
}
|
||||
|
||||
function hint(cm) {
|
||||
return getRules().then(rules => {
|
||||
let ruleIds, options;
|
||||
if (linter === 'stylelint') {
|
||||
ruleIds = Object.keys(rules);
|
||||
options = rules;
|
||||
} else {
|
||||
ruleIds = rules.map(r => r.id);
|
||||
options = {};
|
||||
}
|
||||
const cursor = cm.getCursor();
|
||||
const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
|
||||
const {line, ch} = cursor;
|
||||
|
||||
const quoted = string.startsWith('"');
|
||||
const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
|
||||
const depth = getLexicalDepth(lexical);
|
||||
|
||||
const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
|
||||
let [, prevWord] = search.find(true) || [];
|
||||
let words = [];
|
||||
|
||||
if (depth === 1 && linter === 'stylelint') {
|
||||
words = quoted ? ['rules'] : [];
|
||||
} else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
|
||||
words = ruleIds;
|
||||
} else if (depth === 2 || depth === 3 && lexical.type === ']') {
|
||||
words = !quoted ? ['true', 'false', 'null'] :
|
||||
ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
|
||||
} else if (depth === 4 && prevWord === 'severity') {
|
||||
words = ['error', 'warning'];
|
||||
} else if (depth === 4) {
|
||||
words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
|
||||
} else if (depth === 5 && lexical.type === ']' && quoted) {
|
||||
while (prevWord && !ruleIds.includes(prevWord)) {
|
||||
prevWord = (search.find(true) || [])[1];
|
||||
}
|
||||
words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
|
||||
}
|
||||
return {
|
||||
list: words.filter(word => word.startsWith(leftPart)),
|
||||
from: {line, ch: start + (quoted ? 1 : 0)},
|
||||
to: {line, ch: string.endsWith('"') ? end - 1 : end},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getLexicalDepth(lexicalState) {
|
||||
let depth = 0;
|
||||
while ((lexicalState = lexicalState.prev)) {
|
||||
depth++;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
})();
|
219
edit/linter-defaults.js
Normal file
219
edit/linter-defaults.js
Normal file
|
@ -0,0 +1,219 @@
|
|||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var LINTER_DEFAULTS = (() => {
|
||||
const SEVERITY = {severity: 'warning'};
|
||||
const STYLELINT = {
|
||||
// 'sugarss' is a indent-based syntax like Sass or Stylus
|
||||
// ref: https://github.com/postcss/postcss#syntaxes
|
||||
// syntax: 'sugarss',
|
||||
// ** recommended rules **
|
||||
// ref: https://github.com/stylelint/stylelint-config-recommended/blob/master/index.js
|
||||
rules: {
|
||||
'at-rule-no-unknown': [true, SEVERITY],
|
||||
'block-no-empty': [true, SEVERITY],
|
||||
'color-no-invalid-hex': [true, SEVERITY],
|
||||
'declaration-block-no-duplicate-properties': [true, {
|
||||
'ignore': ['consecutive-duplicates-with-different-values'],
|
||||
'severity': 'warning'
|
||||
}],
|
||||
'declaration-block-no-shorthand-property-overrides': [true, SEVERITY],
|
||||
'font-family-no-duplicate-names': [true, SEVERITY],
|
||||
'function-calc-no-unspaced-operator': [true, SEVERITY],
|
||||
'function-linear-gradient-no-nonstandard-direction': [true, SEVERITY],
|
||||
'keyframe-declaration-no-important': [true, SEVERITY],
|
||||
'media-feature-name-no-unknown': [true, SEVERITY],
|
||||
/* recommended true */
|
||||
'no-empty-source': false,
|
||||
'no-extra-semicolons': [true, SEVERITY],
|
||||
'no-invalid-double-slash-comments': [true, SEVERITY],
|
||||
'property-no-unknown': [true, SEVERITY],
|
||||
'selector-pseudo-class-no-unknown': [true, SEVERITY],
|
||||
'selector-pseudo-element-no-unknown': [true, SEVERITY],
|
||||
'selector-type-no-unknown': false, // for scss/less/stylus-lang
|
||||
'string-no-newline': [true, SEVERITY],
|
||||
'unit-no-unknown': [true, SEVERITY],
|
||||
|
||||
// ** non-essential rules
|
||||
'comment-no-empty': false,
|
||||
'declaration-block-no-redundant-longhand-properties': false,
|
||||
'shorthand-property-no-redundant-values': false,
|
||||
|
||||
// ** stylistic rules **
|
||||
/*
|
||||
'at-rule-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'blockless-after-same-name-blockless',
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment'
|
||||
]
|
||||
}
|
||||
],
|
||||
'at-rule-name-case': 'lower',
|
||||
'at-rule-name-space-after': 'always-single-line',
|
||||
'at-rule-semicolon-newline-after': 'always',
|
||||
'block-closing-brace-empty-line-before': 'never',
|
||||
'block-closing-brace-newline-after': 'always',
|
||||
'block-closing-brace-newline-before': 'always-multi-line',
|
||||
'block-closing-brace-space-before': 'always-single-line',
|
||||
'block-opening-brace-newline-after': 'always-multi-line',
|
||||
'block-opening-brace-space-after': 'always-single-line',
|
||||
'block-opening-brace-space-before': 'always',
|
||||
'color-hex-case': 'lower',
|
||||
'color-hex-length': 'short',
|
||||
'comment-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'stylelint-commands'
|
||||
]
|
||||
}
|
||||
],
|
||||
'comment-whitespace-inside': 'always',
|
||||
'custom-property-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'after-custom-property',
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment',
|
||||
'inside-single-line-block'
|
||||
]
|
||||
}
|
||||
],
|
||||
'declaration-bang-space-after': 'never',
|
||||
'declaration-bang-space-before': 'always',
|
||||
'declaration-block-semicolon-newline-after': 'always-multi-line',
|
||||
'declaration-block-semicolon-space-after': 'always-single-line',
|
||||
'declaration-block-semicolon-space-before': 'never',
|
||||
'declaration-block-single-line-max-declarations': 1,
|
||||
'declaration-block-trailing-semicolon': 'always',
|
||||
'declaration-colon-newline-after': 'always-multi-line',
|
||||
'declaration-colon-space-after': 'always-single-line',
|
||||
'declaration-colon-space-before': 'never',
|
||||
'declaration-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
'except': [
|
||||
'after-declaration',
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment',
|
||||
'inside-single-line-block'
|
||||
]
|
||||
}
|
||||
],
|
||||
'function-comma-newline-after': 'always-multi-line',
|
||||
'function-comma-space-after': 'always-single-line',
|
||||
'function-comma-space-before': 'never',
|
||||
'function-max-empty-lines': 0,
|
||||
'function-name-case': 'lower',
|
||||
'function-parentheses-newline-inside': 'always-multi-line',
|
||||
'function-parentheses-space-inside': 'never-single-line',
|
||||
'function-whitespace-after': 'always',
|
||||
'indentation': 2,
|
||||
'length-zero-no-unit': true,
|
||||
'max-empty-lines': 1,
|
||||
'media-feature-colon-space-after': 'always',
|
||||
'media-feature-colon-space-before': 'never',
|
||||
'media-feature-name-case': 'lower',
|
||||
'media-feature-parentheses-space-inside': 'never',
|
||||
'media-feature-range-operator-space-after': 'always',
|
||||
'media-feature-range-operator-space-before': 'always',
|
||||
'media-query-list-comma-newline-after': 'always-multi-line',
|
||||
'media-query-list-comma-space-after': 'always-single-line',
|
||||
'media-query-list-comma-space-before': 'never',
|
||||
'no-eol-whitespace': true,
|
||||
'no-missing-end-of-source-newline': true,
|
||||
'number-leading-zero': 'always',
|
||||
'number-no-trailing-zeros': true,
|
||||
'property-case': 'lower',
|
||||
'rule-empty-line-before': [
|
||||
'always-multi-line',
|
||||
{
|
||||
'except': [
|
||||
'first-nested'
|
||||
],
|
||||
'ignore': [
|
||||
'after-comment'
|
||||
]
|
||||
}
|
||||
],
|
||||
'selector-attribute-brackets-space-inside': 'never',
|
||||
'selector-attribute-operator-space-after': 'never',
|
||||
'selector-attribute-operator-space-before': 'never',
|
||||
'selector-combinator-space-after': 'always',
|
||||
'selector-combinator-space-before': 'always',
|
||||
'selector-descendant-combinator-no-non-space': true,
|
||||
'selector-list-comma-newline-after': 'always',
|
||||
'selector-list-comma-space-before': 'never',
|
||||
'selector-max-empty-lines': 0,
|
||||
'selector-pseudo-class-case': 'lower',
|
||||
'selector-pseudo-class-parentheses-space-inside': 'never',
|
||||
'selector-pseudo-element-case': 'lower',
|
||||
'selector-pseudo-element-colon-notation': 'double',
|
||||
'selector-type-case': 'lower',
|
||||
'unit-case': 'lower',
|
||||
'value-list-comma-newline-after': 'always-multi-line',
|
||||
'value-list-comma-space-after': 'always-single-line',
|
||||
'value-list-comma-space-before': 'never',
|
||||
'value-list-max-empty-lines': 0
|
||||
*/
|
||||
}
|
||||
};
|
||||
const CSSLINT = {
|
||||
// Default warnings
|
||||
'display-property-grouping': 1,
|
||||
'duplicate-properties': 1,
|
||||
'empty-rules': 1,
|
||||
'errors': 1,
|
||||
'warnings': 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
|
||||
};
|
||||
return {STYLELINT, CSSLINT, SEVERITY};
|
||||
})();
|
113
edit/linter-engines.js
Normal file
113
edit/linter-engines.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
/* global LINTER_DEFAULTS linter editorWorker */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
registerLinters({
|
||||
csslint: {
|
||||
storageName: 'editorCSSLintConfig',
|
||||
lint: csslint,
|
||||
validMode: mode => mode === 'css',
|
||||
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config)
|
||||
},
|
||||
stylelint: {
|
||||
storageName: 'editorStylelintConfig',
|
||||
lint: stylelint,
|
||||
validMode: () => true,
|
||||
getConfig: config => ({
|
||||
syntax: 'sugarss',
|
||||
rules: Object.assign({}, LINTER_DEFAULTS.STYLELINT.rules, config && config.rules)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
function stylelint(text, config, mode) {
|
||||
return editorWorker.stylelint(text, config)
|
||||
.then(({results}) => {
|
||||
if (!results[0]) {
|
||||
return [];
|
||||
}
|
||||
const output = results[0].warnings.map(({line, column: ch, text, severity}) =>
|
||||
({
|
||||
from: {line: line - 1, ch: ch - 1},
|
||||
to: {line: line - 1, ch},
|
||||
message: text
|
||||
.replace('Unexpected ', '')
|
||||
.replace(/^./, firstLetter => firstLetter.toUpperCase())
|
||||
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
|
||||
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
|
||||
severity,
|
||||
})
|
||||
);
|
||||
return mode !== 'stylus' ?
|
||||
output :
|
||||
output.filter(({message}) =>
|
||||
!message.includes('"@css"') || !message.includes('(at-rule-no-unknown)'));
|
||||
});
|
||||
}
|
||||
|
||||
function csslint(text, config) {
|
||||
return editorWorker.csslint(text, config)
|
||||
.then(results =>
|
||||
results
|
||||
.map(({line, col: ch, message, rule, type: severity}) => line && {
|
||||
message,
|
||||
from: {line: line - 1, ch: ch - 1},
|
||||
to: {line: line - 1, ch},
|
||||
rule: rule.id,
|
||||
severity,
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
function registerLinters(engines) {
|
||||
const configs = new Map();
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'sync') {
|
||||
return;
|
||||
}
|
||||
for (const [name, engine] of Object.entries(engines)) {
|
||||
if (changes.hasOwnProperty(engine.storageName)) {
|
||||
chromeSync.getLZValue(engine.storageName)
|
||||
.then(config => {
|
||||
configs.set(name, engine.getConfig(config));
|
||||
linter.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
linter.register((text, options, cm) => {
|
||||
const selectedLinter = prefs.get('editor.linter');
|
||||
if (!selectedLinter) {
|
||||
return;
|
||||
}
|
||||
const mode = cm.getOption('mode');
|
||||
if (engines[selectedLinter].validMode(mode)) {
|
||||
return runLint(selectedLinter);
|
||||
}
|
||||
for (const [name, engine] of Object.entries(engines)) {
|
||||
if (engine.validMode(mode)) {
|
||||
return runLint(name);
|
||||
}
|
||||
}
|
||||
|
||||
function runLint(name) {
|
||||
return getConfig(name)
|
||||
.then(config => engines[name].lint(text, config, mode));
|
||||
}
|
||||
});
|
||||
|
||||
function getConfig(name) {
|
||||
if (configs.has(name)) {
|
||||
return Promise.resolve(configs.get(name));
|
||||
}
|
||||
return chromeSync.getLZValue(engines[name].storageName)
|
||||
.then(config => {
|
||||
configs.set(name, engines[name].getConfig(config));
|
||||
return configs.get(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
51
edit/linter-help-dialog.js
Normal file
51
edit/linter-help-dialog.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/* global showHelp editorWorker memoize */
|
||||
'use strict';
|
||||
|
||||
function createLinterHelpDialog(getIssues) {
|
||||
let csslintRules;
|
||||
const prepareCsslintRules = memoize(() =>
|
||||
editorWorker.getCsslintRules()
|
||||
.then(rules => {
|
||||
csslintRules = rules;
|
||||
})
|
||||
);
|
||||
return {show};
|
||||
|
||||
function show() {
|
||||
// FIXME: implement a linterChooser?
|
||||
const linter = $('#editor.linter').value;
|
||||
const baseUrl = linter === 'stylelint'
|
||||
? 'https://stylelint.io/user-guide/rules/'
|
||||
// some CSSLint rules do not have a url
|
||||
: 'https://github.com/CSSLint/csslint/issues/535';
|
||||
let headerLink, template;
|
||||
if (linter === 'csslint') {
|
||||
headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules', 'CSSLint');
|
||||
template = ({rule: ruleID}) => {
|
||||
const rule = csslintRules.find(rule => rule.id === ruleID);
|
||||
return rule &&
|
||||
$create('li', [
|
||||
$create('b', $createLink(rule.url || baseUrl, rule.name)),
|
||||
$create('br'),
|
||||
rule.desc,
|
||||
]);
|
||||
};
|
||||
} else {
|
||||
headerLink = $createLink(baseUrl, 'stylelint');
|
||||
template = ({rule}) =>
|
||||
$create('li',
|
||||
rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
|
||||
}
|
||||
const header = t('linterIssuesHelp', '\x01').split('\x01');
|
||||
const activeRules = getIssues();
|
||||
Promise.resolve(linter === 'csslint' && prepareCsslintRules())
|
||||
.then(() =>
|
||||
showHelp(t('linterIssues'),
|
||||
$create([
|
||||
header[0], headerLink, header[1],
|
||||
$create('ul.rules', [...activeRules.values()].map(template)),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
47
edit/linter-meta.js
Normal file
47
edit/linter-meta.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/* global linter */
|
||||
'use strict';
|
||||
|
||||
function createMetaCompiler(cm) {
|
||||
const updateListeners = [];
|
||||
let meta = null;
|
||||
let metaIndex = null;
|
||||
let cache = [];
|
||||
|
||||
linter.register((text, options, _cm) => {
|
||||
if (_cm !== cm) {
|
||||
return;
|
||||
}
|
||||
const match = text.match(/\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
if (match[0] === meta && match.index === metaIndex) {
|
||||
return cache;
|
||||
}
|
||||
return API.parseUsercss({sourceCode: match[0], metaOnly: true})
|
||||
.then(result => result.usercssData)
|
||||
.then(result => {
|
||||
for (const cb of updateListeners) {
|
||||
cb(result);
|
||||
}
|
||||
meta = match[0];
|
||||
metaIndex = match.index;
|
||||
cache = [];
|
||||
return cache;
|
||||
}, err => {
|
||||
meta = match[0];
|
||||
metaIndex = match.index;
|
||||
cache = [{
|
||||
from: cm.posFromIndex((err.index || 0) + match.index),
|
||||
to: cm.posFromIndex((err.index || 0) + match.index),
|
||||
message: err.message,
|
||||
severity: 'error'
|
||||
}];
|
||||
return cache;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
onUpdated: cb => updateListeners.push(cb)
|
||||
};
|
||||
}
|
165
edit/linter-report.js
Normal file
165
edit/linter-report.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
/* global linter editors clipString createLinterHelpDialog makeSectionVisible */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
Object.assign(linter, (() => {
|
||||
const tables = new Map();
|
||||
const helpDialog = createLinterHelpDialog(getIssues);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
$('#lint-help').addEventListener('click', helpDialog.show);
|
||||
}, {once: true});
|
||||
|
||||
linter.onLintingUpdated((annotationsNotSorted, annotations, cm) => {
|
||||
let table = tables.get(cm);
|
||||
if (!table) {
|
||||
table = createTable(cm);
|
||||
tables.set(cm, table);
|
||||
const container = $('.lint-report-container');
|
||||
if (typeof editor === 'object') {
|
||||
container.append(table.element);
|
||||
} else {
|
||||
const nextSibling = findNextSibling(tables, cm);
|
||||
container.insertBefore(table.element, nextSibling && tables.get(nextSibling).element);
|
||||
}
|
||||
}
|
||||
table.updateCaption();
|
||||
table.updateAnnotations(annotations);
|
||||
updateCount();
|
||||
});
|
||||
|
||||
linter.onUnhook(cm => {
|
||||
const table = tables.get(cm);
|
||||
if (table) {
|
||||
table.element.remove();
|
||||
tables.delete(cm);
|
||||
}
|
||||
updateCount();
|
||||
});
|
||||
|
||||
return {refreshReport};
|
||||
|
||||
function updateCount() {
|
||||
const issueCount = Array.from(tables.values())
|
||||
.reduce((sum, table) => sum + table.trs.length, 0);
|
||||
$('#lint').classList.toggle('hidden', issueCount === 0);
|
||||
$('#issue-count').textContent = issueCount;
|
||||
}
|
||||
|
||||
function getIssues() {
|
||||
const issues = new Set();
|
||||
for (const table of tables.values()) {
|
||||
for (const tr of table.trs) {
|
||||
issues.add(tr.getAnnotation());
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function findNextSibling(tables, cm) {
|
||||
let i = editors.indexOf(cm) + 1;
|
||||
while (i < editors.length) {
|
||||
if (tables.has(editors[i])) {
|
||||
return editors[i];
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshReport() {
|
||||
for (const table of tables.values()) {
|
||||
table.updateCaption();
|
||||
}
|
||||
}
|
||||
|
||||
function createTable(cm) {
|
||||
const caption = $create('caption');
|
||||
const tbody = $create('tbody');
|
||||
const table = $create('table', [caption, tbody]);
|
||||
const trs = [];
|
||||
return {
|
||||
element: table,
|
||||
trs,
|
||||
updateAnnotations,
|
||||
updateCaption
|
||||
};
|
||||
|
||||
function updateCaption() {
|
||||
caption.textContent = typeof editor === 'object' ?
|
||||
'' : `${t('sectionCode')} ${editors.indexOf(cm) + 1}`;
|
||||
}
|
||||
|
||||
function updateAnnotations(lines) {
|
||||
let i = 0;
|
||||
for (const anno of getAnnotations()) {
|
||||
let tr;
|
||||
if (i < trs.length) {
|
||||
tr = trs[i];
|
||||
} else {
|
||||
tr = createTr();
|
||||
trs.push(tr);
|
||||
tbody.append(tr.element);
|
||||
}
|
||||
tr.update(anno);
|
||||
i++;
|
||||
}
|
||||
if (i === 0) {
|
||||
trs.length = 0;
|
||||
tbody.textContent = '';
|
||||
} else {
|
||||
while (trs.length > i) {
|
||||
trs.pop().element.remove();
|
||||
}
|
||||
}
|
||||
table.classList.toggle('empty', trs.length === 0);
|
||||
|
||||
function *getAnnotations() {
|
||||
for (const line of lines.filter(Boolean)) {
|
||||
yield *line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTr() {
|
||||
let anno;
|
||||
const severityIcon = $create('div');
|
||||
const severity = $create('td', {attributes: {role: 'severity'}}, severityIcon);
|
||||
const line = $create('td', {attributes: {role: 'line'}});
|
||||
const col = $create('td', {attributes: {role: 'col'}});
|
||||
const message = $create('td', {attributes: {role: 'message'}});
|
||||
|
||||
const trElement = $create('tr', {
|
||||
onclick: () => gotoLintIssue(cm, anno)
|
||||
}, [
|
||||
severity,
|
||||
line,
|
||||
$create('td', {attributes: {role: 'sep'}}, ':'),
|
||||
col,
|
||||
message
|
||||
]);
|
||||
return {
|
||||
element: trElement,
|
||||
update,
|
||||
getAnnotation: () => anno
|
||||
};
|
||||
|
||||
function update(_anno) {
|
||||
anno = _anno;
|
||||
trElement.className = anno.severity;
|
||||
severity.dataset.rule = anno.rule;
|
||||
severityIcon.className = `CodeMirror-lint-marker-${anno.severity}`;
|
||||
severityIcon.textContent = anno.severity;
|
||||
line.textContent = anno.from.line + 1;
|
||||
col.textContent = anno.from.ch + 1;
|
||||
message.title = clipString(anno.message, 1000) + `\n(${anno.rule})`;
|
||||
message.textContent = clipString(anno.message, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gotoLintIssue(cm, anno) {
|
||||
makeSectionVisible(cm);
|
||||
cm.focus();
|
||||
cm.setSelection(anno.from);
|
||||
}
|
||||
})());
|
65
edit/linter.js
Normal file
65
edit/linter.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var linter = (() => {
|
||||
const lintingUpdatedListeners = [];
|
||||
const unhookListeners = [];
|
||||
const linters = [];
|
||||
const cms = new Set();
|
||||
|
||||
return {
|
||||
register,
|
||||
run,
|
||||
enableForEditor,
|
||||
disableForEditor,
|
||||
onLintingUpdated,
|
||||
onUnhook
|
||||
};
|
||||
|
||||
function onUnhook(cb) {
|
||||
unhookListeners.push(cb);
|
||||
}
|
||||
|
||||
function onLintingUpdated(cb) {
|
||||
lintingUpdatedListeners.push(cb);
|
||||
}
|
||||
|
||||
function onUpdateLinting(...args) {
|
||||
for (const cb of lintingUpdatedListeners) {
|
||||
cb(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function enableForEditor(cm) {
|
||||
cm.setOption('lint', {onUpdateLinting, getAnnotations});
|
||||
cms.add(cm);
|
||||
}
|
||||
|
||||
function disableForEditor(cm) {
|
||||
cm.setOption('lint', false);
|
||||
cms.delete(cm);
|
||||
for (const cb of unhookListeners) {
|
||||
cb(cm);
|
||||
}
|
||||
}
|
||||
|
||||
function register(linterFn) {
|
||||
linters.push(linterFn);
|
||||
}
|
||||
|
||||
function run() {
|
||||
for (const cm of cms) {
|
||||
cm.performLint();
|
||||
}
|
||||
}
|
||||
|
||||
function getAnnotations(...args) {
|
||||
return Promise.all(linters.map(fn => fn(...args)))
|
||||
.then(results => [].concat(...results.filter(Boolean)));
|
||||
}
|
||||
})();
|
||||
|
||||
// FIXME: this should be put inside edit.js
|
||||
prefs.subscribe(['editor.linter'], () => {
|
||||
linter.run();
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
global CodeMirror
|
||||
global editors propertyToCss CssToProperty
|
||||
global onChange indicateCodeChange initHooks setCleanGlobal
|
||||
global onChange initHooks setCleanGlobal
|
||||
global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete
|
||||
global setCleanItem updateTitle updateLintReportIfEnabled renderLintReport
|
||||
global setCleanItem updateTitle
|
||||
global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
|
||||
global clipString
|
||||
global clipString linter
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -146,12 +146,13 @@ function addSection(event, section) {
|
|||
const newIndex = getSections().indexOf(clickedSection) + 1;
|
||||
cm = setupCodeMirror(div, code, newIndex);
|
||||
makeSectionVisible(cm);
|
||||
renderLintReport();
|
||||
cm.focus();
|
||||
} else {
|
||||
sections.appendChild(div);
|
||||
cm = setupCodeMirror(div, code);
|
||||
}
|
||||
linter.enableForEditor(cm);
|
||||
linter.refreshReport();
|
||||
div.CodeMirror = cm;
|
||||
setCleanSection(div);
|
||||
return div;
|
||||
|
@ -308,7 +309,6 @@ function indicateCodeChange(cm) {
|
|||
const section = cm.getSection();
|
||||
setCleanItem(section, cm.isClean(section.savedValue));
|
||||
updateTitle();
|
||||
updateLintReportIfEnabled(cm);
|
||||
}
|
||||
|
||||
function setupAutocomplete(cm, enable = true) {
|
||||
|
@ -481,13 +481,16 @@ function removeSection(event) {
|
|||
setCleanItem(section, false);
|
||||
updateTitle();
|
||||
cm.focus();
|
||||
linter.enableForEditor(cm);
|
||||
linter.refreshReport();
|
||||
};
|
||||
section.insertAdjacentElement('afterend', stub);
|
||||
}
|
||||
setCleanItem($('#sections'), false);
|
||||
removeAreaAndSetDirty(section);
|
||||
editors.splice(editors.indexOf(cm), 1);
|
||||
renderLintReport();
|
||||
linter.disableForEditor(cm);
|
||||
linter.refreshReport();
|
||||
}
|
||||
|
||||
function removeAreaAndSetDirty(area) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
global editors styleId: true
|
||||
global CodeMirror dirtyReporter
|
||||
global updateLintReportIfEnabled initLint linterConfig updateLinter
|
||||
global createAppliesToLineWidget messageBox
|
||||
global sectionsToMozFormat
|
||||
global beforeUnload
|
||||
global createMetaCompiler linter
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -44,7 +44,6 @@ function createSourceEditor(style) {
|
|||
|
||||
cm.on('changes', () => {
|
||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||
updateLintReportIfEnabled(cm);
|
||||
});
|
||||
|
||||
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
|
||||
|
@ -55,9 +54,17 @@ function createSourceEditor(style) {
|
|||
|
||||
cm.operation(initAppliesToLineWidget);
|
||||
|
||||
updateMeta().then(() => {
|
||||
const metaCompiler = createMetaCompiler(cm);
|
||||
metaCompiler.onUpdated(meta => {
|
||||
style.usercssData = meta;
|
||||
style.name = meta.name;
|
||||
style.url = meta.homepageURL;
|
||||
updateMeta();
|
||||
});
|
||||
|
||||
initLint();
|
||||
linter.enableForEditor(cm);
|
||||
|
||||
updateMeta().then(() => {
|
||||
|
||||
let prevMode = NaN;
|
||||
cm.on('optionChange', (cm, option) => {
|
||||
|
@ -65,7 +72,7 @@ function createSourceEditor(style) {
|
|||
const mode = getModeName();
|
||||
if (mode === prevMode) return;
|
||||
prevMode = mode;
|
||||
updateLinter();
|
||||
linter.run();
|
||||
updateLinterSwitch();
|
||||
});
|
||||
|
||||
|
@ -88,7 +95,7 @@ function createSourceEditor(style) {
|
|||
|
||||
function updateLinterSwitch() {
|
||||
const el = $('#editor.linter');
|
||||
el.value = linterConfig.getName();
|
||||
el.value = getCurrentLinter();
|
||||
const cssLintOption = $('[value="csslint"]', el);
|
||||
const mode = getModeName();
|
||||
if (mode !== 'css') {
|
||||
|
@ -100,6 +107,14 @@ function createSourceEditor(style) {
|
|||
}
|
||||
}
|
||||
|
||||
function getCurrentLinter() {
|
||||
const name = prefs.get('editor.linter');
|
||||
if (cm.getOption('mode') !== 'css' && name === 'csslint') {
|
||||
return 'stylelint';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function setupNewStyle(style) {
|
||||
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
|
||||
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/* global require importScripts */
|
||||
'use strict';
|
||||
|
||||
importScripts('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||
|
||||
const stylelint = require('stylelint');
|
||||
|
||||
self.onmessage = ({data: {action = 'run', code, config}}) => {
|
||||
switch (action) {
|
||||
case 'getAllRuleIds':
|
||||
// the functions are non-tranferable
|
||||
self.postMessage(Object.keys(stylelint.rules));
|
||||
return;
|
||||
case 'getAllRuleOptions':
|
||||
self.postMessage(getAllRuleOptions());
|
||||
return;
|
||||
case 'run':
|
||||
stylelint.lint({code, config}).then(results =>
|
||||
self.postMessage(results));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
function getAllRuleOptions() {
|
||||
const options = {};
|
||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||
for (const id of Object.keys(stylelint.rules)) {
|
||||
const ruleCode = String(stylelint.rules[id]);
|
||||
const sets = [];
|
||||
let m, mStr;
|
||||
while ((m = rxPossible.exec(ruleCode))) {
|
||||
const possible = m[1];
|
||||
const set = [];
|
||||
while ((mStr = rxString.exec(possible))) {
|
||||
const s = mStr[1];
|
||||
if (s.includes(' ')) {
|
||||
set.push(...s.split(/\s+/));
|
||||
} else {
|
||||
set.push(s);
|
||||
}
|
||||
}
|
||||
if (possible.includes('ignoreAtRules')) {
|
||||
set.push('ignoreAtRules');
|
||||
}
|
||||
if (possible.includes('ignoreShorthands')) {
|
||||
set.push('ignoreShorthands');
|
||||
}
|
||||
if (set.length) {
|
||||
sets.push(set);
|
||||
}
|
||||
}
|
||||
if (sets.length) {
|
||||
options[id] = sets;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
13
edit/util.js
13
edit/util.js
|
@ -121,3 +121,16 @@ function sectionsToMozFormat(style) {
|
|||
function clipString(str, limit = 100) {
|
||||
return str.length <= limit ? str : str.substr(0, limit) + '...';
|
||||
}
|
||||
|
||||
// this is a decorator. Cache the first call
|
||||
function memoize(fn) {
|
||||
let cached = false;
|
||||
let result;
|
||||
return (...args) => {
|
||||
if (!cached) {
|
||||
result = fn(...args);
|
||||
cached = true;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -632,7 +632,7 @@ var usercss = (() => {
|
|||
|
||||
function invokeWorker(message) {
|
||||
if (!worker.queue) {
|
||||
worker.instance = new Worker('/edit/csslint-loader.js');
|
||||
worker.instance = new Worker('/background/parserlib-loader.js');
|
||||
worker.queue = [];
|
||||
worker.instance.onmessage = ({data}) => {
|
||||
worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"updates": "^4.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint **/*.js || true",
|
||||
"lint": "eslint **/*.js --cache || exit 0",
|
||||
"update": "npm run update-node && npm run update-main",
|
||||
"update-quick": "updates -u && npm update && npm run update-main",
|
||||
"update-main": "npm run update-versions && npm run update-codemirror",
|
||||
|
|
|
@ -425,7 +425,7 @@ CSSLint.addRule({
|
|||
}
|
||||
};
|
||||
|
||||
CSSLint.util.registerBlockEvents(parser, startRule, endRule, property);
|
||||
CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user