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
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
*.zip
|
*.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/script-loader.js"></script>
|
||||||
<script src="js/storage-util.js"></script>
|
<script src="js/storage-util.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
<script src="edit/lint.js"></script>
|
|
||||||
<script src="edit/util.js"></script>
|
<script src="edit/util.js"></script>
|
||||||
<script src="edit/regexp-tester.js"></script>
|
<script src="edit/regexp-tester.js"></script>
|
||||||
<script src="edit/applies-to-line-widget.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>
|
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||||
|
|
||||||
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet" />
|
<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" />
|
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet" />
|
||||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||||
|
@ -87,6 +88,16 @@
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<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">
|
<link id="cm-theme" rel="stylesheet">
|
||||||
|
|
||||||
<template data-id="appliesTo">
|
<template data-id="appliesTo">
|
||||||
|
@ -417,7 +428,7 @@
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
</summary>
|
</summary>
|
||||||
<div></div>
|
<div class="lint-report-container"></div>
|
||||||
</details>
|
</details>
|
||||||
<div id="footer" class="hidden">
|
<div id="footer" class="hidden">
|
||||||
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
|
<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 editors editor styleId ownTabId
|
||||||
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
|
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
|
||||||
global getSectionsHashes
|
global getSectionsHashes
|
||||||
|
@ -8,9 +8,6 @@ global messageBox
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMscriptReady('/codemirror.js').then(() => {
|
onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
|
|
||||||
CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
|
|
||||||
|
|
||||||
const COMMANDS = {
|
const COMMANDS = {
|
||||||
save,
|
save,
|
||||||
toggleStyle,
|
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 {
|
#lint table:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
#lint table.empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#lint caption {
|
#lint caption {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
31
edit/edit.js
31
edit/edit.js
|
@ -1,13 +1,12 @@
|
||||||
/*
|
/*
|
||||||
global CodeMirror parserlib loadScript
|
global CodeMirror loadScript
|
||||||
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
|
|
||||||
global createSourceEditor
|
global createSourceEditor
|
||||||
global closeCurrentTab regExpTester messageBox
|
global closeCurrentTab regExpTester messageBox
|
||||||
global setupCodeMirror
|
global setupCodeMirror
|
||||||
global beautify
|
global beautify
|
||||||
global initWithSectionStyle addSections removeSection getSectionsHashes
|
global initWithSectionStyle addSections removeSection getSectionsHashes
|
||||||
global sectionsToMozFormat
|
global sectionsToMozFormat
|
||||||
global moveFocus
|
global moveFocus editorWorker
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -212,7 +211,6 @@ function beforeUnload() {
|
||||||
}
|
}
|
||||||
const isDirty = editor ? editor.isDirty() : !isCleanGlobal();
|
const isDirty = editor ? editor.isDirty() : !isCleanGlobal();
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
updateLintReportIfEnabled(null, 0);
|
|
||||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||||
return t('styleChangesNotSaved');
|
return t('styleChangesNotSaved');
|
||||||
}
|
}
|
||||||
|
@ -276,9 +274,6 @@ function initHooks() {
|
||||||
$('#save-button').addEventListener('click', save, false);
|
$('#save-button').addEventListener('click', save, false);
|
||||||
$('#sections-help').addEventListener('click', showSectionHelp, false);
|
$('#sections-help').addEventListener('click', showSectionHelp, false);
|
||||||
|
|
||||||
// TODO: investigate why FF needs this delay
|
|
||||||
debounce(initLint, FIREFOX ? 100 : 0);
|
|
||||||
|
|
||||||
if (!FIREFOX) {
|
if (!FIREFOX) {
|
||||||
$$([
|
$$([
|
||||||
'input:not([type])',
|
'input:not([type])',
|
||||||
|
@ -353,7 +348,6 @@ function toggleStyle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
updateLintReportIfEnabled(null, 0);
|
|
||||||
if (!validate()) {
|
if (!validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -414,12 +408,6 @@ function updateTitle() {
|
||||||
$('#save-button').disabled = clean;
|
$('#save-button').disabled = clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLintReportIfEnabled(...args) {
|
|
||||||
if (CodeMirror.defaults.lint) {
|
|
||||||
updateLintReport(...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMozillaFormat() {
|
function showMozillaFormat() {
|
||||||
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
||||||
popup.codebox.setValue(toMozillaFormat());
|
popup.codebox.setValue(toMozillaFormat());
|
||||||
|
@ -461,16 +449,7 @@ function fromMozillaFormat() {
|
||||||
|
|
||||||
function doImport({replaceOldStyle = false}) {
|
function doImport({replaceOldStyle = false}) {
|
||||||
lockPageUI(true);
|
lockPageUI(true);
|
||||||
new Promise(setTimeout)
|
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
|
||||||
.then(() => {
|
|
||||||
const worker = linterConfig.worker.csslint;
|
|
||||||
if (!worker.instance) worker.instance = new Worker(worker.path);
|
|
||||||
})
|
|
||||||
.then(() => linterConfig.invokeWorker({
|
|
||||||
linter: 'csslint',
|
|
||||||
action: 'parse',
|
|
||||||
code: popup.codebox.getValue().trim(),
|
|
||||||
}))
|
|
||||||
.then(({sections, errors}) => {
|
.then(({sections, errors}) => {
|
||||||
// shouldn't happen but just in case
|
// shouldn't happen but just in case
|
||||||
if (!sections.length && errors.length) {
|
if (!sections.length && errors.length) {
|
||||||
|
@ -483,8 +462,7 @@ function fromMozillaFormat() {
|
||||||
removeOldSections(replaceOldStyle);
|
removeOldSections(replaceOldStyle);
|
||||||
return addSections(sections, div => setCleanItem(div, false));
|
return addSections(sections, div => setCleanItem(div, false));
|
||||||
})
|
})
|
||||||
.then(sectionDivs => {
|
.then(() => {
|
||||||
sectionDivs.forEach(div => updateLintReportIfEnabled(div.CodeMirror, 1));
|
|
||||||
$('.dismiss').dispatchEvent(new Event('click'));
|
$('.dismiss').dispatchEvent(new Event('click'));
|
||||||
})
|
})
|
||||||
.catch(showError)
|
.catch(showError)
|
||||||
|
@ -604,7 +582,6 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
lint: linterConfig.getForCodeMirror(),
|
|
||||||
styleActiveLine: true,
|
styleActiveLine: true,
|
||||||
theme: prefs.get('editor.theme'),
|
theme: prefs.get('editor.theme'),
|
||||||
keyMap: prefs.get('editor.keyMap')
|
keyMap: prefs.get('editor.keyMap')
|
||||||
|
|
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 CodeMirror
|
||||||
global editors propertyToCss CssToProperty
|
global editors propertyToCss CssToProperty
|
||||||
global onChange indicateCodeChange initHooks setCleanGlobal
|
global onChange initHooks setCleanGlobal
|
||||||
global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete
|
global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete
|
||||||
global setCleanItem updateTitle updateLintReportIfEnabled renderLintReport
|
global setCleanItem updateTitle
|
||||||
global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
|
global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
|
||||||
global clipString
|
global clipString linter
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -146,12 +146,13 @@ function addSection(event, section) {
|
||||||
const newIndex = getSections().indexOf(clickedSection) + 1;
|
const newIndex = getSections().indexOf(clickedSection) + 1;
|
||||||
cm = setupCodeMirror(div, code, newIndex);
|
cm = setupCodeMirror(div, code, newIndex);
|
||||||
makeSectionVisible(cm);
|
makeSectionVisible(cm);
|
||||||
renderLintReport();
|
|
||||||
cm.focus();
|
cm.focus();
|
||||||
} else {
|
} else {
|
||||||
sections.appendChild(div);
|
sections.appendChild(div);
|
||||||
cm = setupCodeMirror(div, code);
|
cm = setupCodeMirror(div, code);
|
||||||
}
|
}
|
||||||
|
linter.enableForEditor(cm);
|
||||||
|
linter.refreshReport();
|
||||||
div.CodeMirror = cm;
|
div.CodeMirror = cm;
|
||||||
setCleanSection(div);
|
setCleanSection(div);
|
||||||
return div;
|
return div;
|
||||||
|
@ -308,7 +309,6 @@ function indicateCodeChange(cm) {
|
||||||
const section = cm.getSection();
|
const section = cm.getSection();
|
||||||
setCleanItem(section, cm.isClean(section.savedValue));
|
setCleanItem(section, cm.isClean(section.savedValue));
|
||||||
updateTitle();
|
updateTitle();
|
||||||
updateLintReportIfEnabled(cm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupAutocomplete(cm, enable = true) {
|
function setupAutocomplete(cm, enable = true) {
|
||||||
|
@ -481,13 +481,16 @@ function removeSection(event) {
|
||||||
setCleanItem(section, false);
|
setCleanItem(section, false);
|
||||||
updateTitle();
|
updateTitle();
|
||||||
cm.focus();
|
cm.focus();
|
||||||
|
linter.enableForEditor(cm);
|
||||||
|
linter.refreshReport();
|
||||||
};
|
};
|
||||||
section.insertAdjacentElement('afterend', stub);
|
section.insertAdjacentElement('afterend', stub);
|
||||||
}
|
}
|
||||||
setCleanItem($('#sections'), false);
|
setCleanItem($('#sections'), false);
|
||||||
removeAreaAndSetDirty(section);
|
removeAreaAndSetDirty(section);
|
||||||
editors.splice(editors.indexOf(cm), 1);
|
editors.splice(editors.indexOf(cm), 1);
|
||||||
renderLintReport();
|
linter.disableForEditor(cm);
|
||||||
|
linter.refreshReport();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAreaAndSetDirty(area) {
|
function removeAreaAndSetDirty(area) {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
global editors styleId: true
|
global editors styleId: true
|
||||||
global CodeMirror dirtyReporter
|
global CodeMirror dirtyReporter
|
||||||
global updateLintReportIfEnabled initLint linterConfig updateLinter
|
|
||||||
global createAppliesToLineWidget messageBox
|
global createAppliesToLineWidget messageBox
|
||||||
global sectionsToMozFormat
|
global sectionsToMozFormat
|
||||||
global beforeUnload
|
global beforeUnload
|
||||||
|
global createMetaCompiler linter
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -44,7 +44,6 @@ function createSourceEditor(style) {
|
||||||
|
|
||||||
cm.on('changes', () => {
|
cm.on('changes', () => {
|
||||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||||
updateLintReportIfEnabled(cm);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
|
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
|
||||||
|
@ -55,9 +54,17 @@ function createSourceEditor(style) {
|
||||||
|
|
||||||
cm.operation(initAppliesToLineWidget);
|
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;
|
let prevMode = NaN;
|
||||||
cm.on('optionChange', (cm, option) => {
|
cm.on('optionChange', (cm, option) => {
|
||||||
|
@ -65,7 +72,7 @@ function createSourceEditor(style) {
|
||||||
const mode = getModeName();
|
const mode = getModeName();
|
||||||
if (mode === prevMode) return;
|
if (mode === prevMode) return;
|
||||||
prevMode = mode;
|
prevMode = mode;
|
||||||
updateLinter();
|
linter.run();
|
||||||
updateLinterSwitch();
|
updateLinterSwitch();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,7 +95,7 @@ function createSourceEditor(style) {
|
||||||
|
|
||||||
function updateLinterSwitch() {
|
function updateLinterSwitch() {
|
||||||
const el = $('#editor.linter');
|
const el = $('#editor.linter');
|
||||||
el.value = linterConfig.getName();
|
el.value = getCurrentLinter();
|
||||||
const cssLintOption = $('[value="csslint"]', el);
|
const cssLintOption = $('[value="csslint"]', el);
|
||||||
const mode = getModeName();
|
const mode = getModeName();
|
||||||
if (mode !== 'css') {
|
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) {
|
function setupNewStyle(style) {
|
||||||
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
|
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
|
||||||
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
|
`/* ${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) {
|
function clipString(str, limit = 100) {
|
||||||
return str.length <= limit ? str : str.substr(0, limit) + '...';
|
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) {
|
function invokeWorker(message) {
|
||||||
if (!worker.queue) {
|
if (!worker.queue) {
|
||||||
worker.instance = new Worker('/edit/csslint-loader.js');
|
worker.instance = new Worker('/background/parserlib-loader.js');
|
||||||
worker.queue = [];
|
worker.queue = [];
|
||||||
worker.instance.onmessage = ({data}) => {
|
worker.instance.onmessage = ({data}) => {
|
||||||
worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);
|
worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"updates": "^4.2.1"
|
"updates": "^4.2.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint **/*.js || true",
|
"lint": "eslint **/*.js --cache || exit 0",
|
||||||
"update": "npm run update-node && npm run update-main",
|
"update": "npm run update-node && npm run update-main",
|
||||||
"update-quick": "updates -u && npm update && npm run update-main",
|
"update-quick": "updates -u && npm update && npm run update-main",
|
||||||
"update-main": "npm run update-versions && npm run update-codemirror",
|
"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