249 lines
7.7 KiB
JavaScript
249 lines
7.7 KiB
JavaScript
/* global $ $create $createLink messageBoxProxy */// dom.js
|
|
/* global chromeSync */// storage-util.js
|
|
/* global editor */
|
|
/* global helpPopup showCodeMirrorPopup */// util.js
|
|
/* global linterMan */
|
|
/* global t */// localization.js
|
|
/* global tryJSONparse */// toolbox.js
|
|
'use strict';
|
|
|
|
(() => {
|
|
/** @type {{csslint:{}, stylelint:{}}} */
|
|
const RULES = {};
|
|
let cm;
|
|
let defaultConfig;
|
|
let isStylelint;
|
|
let linter;
|
|
let popup;
|
|
|
|
linterMan.showLintConfig = async () => {
|
|
linter = await getLinter();
|
|
if (!linter) {
|
|
return;
|
|
}
|
|
await require([
|
|
'/vendor/codemirror/mode/javascript/javascript',
|
|
'/vendor/codemirror/addon/lint/json-lint',
|
|
'/vendor/jsonlint/jsonlint',
|
|
]);
|
|
const config = await chromeSync.getLZValue(chromeSync.LZ_KEY[linter]);
|
|
const title = t('linterConfigPopupTitle', isStylelint ? 'Stylelint' : 'CSSLint');
|
|
isStylelint = linter === 'stylelint';
|
|
defaultConfig = stringifyConfig(linterMan.DEFAULTS[linter]);
|
|
popup = showCodeMirrorPopup(title, null, {
|
|
extraKeys: {'Ctrl-Enter': onConfigSave},
|
|
hintOptions: {hint},
|
|
lint: true,
|
|
mode: 'application/json',
|
|
value: config ? stringifyConfig(config) : defaultConfig,
|
|
});
|
|
$('.contents', popup).appendChild(
|
|
$create('div', [
|
|
$create('p', [
|
|
$createLink(
|
|
isStylelint
|
|
? '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: onConfigSave, title: 'Ctrl-Enter'},
|
|
t('styleSaveLabel')),
|
|
$create('button.cancel', {onclick: onConfigCancel}, t('confirmClose')),
|
|
$create('button.reset', {onclick: onConfigReset, title: t('linterResetMessage')},
|
|
t('genericResetLabel')),
|
|
]),
|
|
]));
|
|
cm = popup.codebox;
|
|
cm.focus();
|
|
const rulesStr = getActiveRules().join('|');
|
|
if (rulesStr) {
|
|
const rx = new RegExp(`"(${rulesStr})"\\s*:`);
|
|
let line = 0;
|
|
cm.startOperation();
|
|
cm.eachLine(({text}) => {
|
|
const m = rx.exec(text);
|
|
if (m) {
|
|
const ch = m.index + 1;
|
|
cm.markText({line, ch}, {line, ch: ch + m[1].length}, {className: 'active-linter-rule'});
|
|
}
|
|
++line;
|
|
});
|
|
cm.endOperation();
|
|
}
|
|
cm.on('changes', updateConfigButtons);
|
|
updateConfigButtons();
|
|
window.on('closeHelp', onConfigClose, {once: true});
|
|
};
|
|
|
|
linterMan.showLintHelp = async () => {
|
|
const linter = await getLinter();
|
|
const baseUrl = linter === 'stylelint'
|
|
? 'https://stylelint.io/user-guide/rules/'
|
|
: '';
|
|
let headerLink, template;
|
|
if (linter === 'csslint') {
|
|
headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules', 'CSSLint');
|
|
template = ruleID => {
|
|
const rule = RULES.csslint.find(rule => rule.id === ruleID);
|
|
return rule &&
|
|
$create('li', [
|
|
$create('b', ruleID + ': '),
|
|
rule.url ? $createLink(rule.url, rule.name) : $create('span', `"${rule.name}"`),
|
|
$create('p', 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');
|
|
helpPopup.show(t('linterIssues'),
|
|
$create([
|
|
header[0], headerLink, header[1],
|
|
$create('ul.rules', getActiveRules().map(template)),
|
|
$create('button', {onclick: linterMan.showLintConfig}, t('configureStyle')),
|
|
]));
|
|
};
|
|
|
|
function getActiveRules() {
|
|
const all = [...linterMan.getIssues()].map(issue => issue.rule);
|
|
const uniq = new Set(all);
|
|
return [...uniq];
|
|
}
|
|
|
|
function getLexicalDepth(lexicalState) {
|
|
let depth = 0;
|
|
while ((lexicalState = lexicalState.prev)) {
|
|
depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
async function getLinter() {
|
|
const val = $('#editor.linter').value;
|
|
if (val && !RULES[val]) {
|
|
RULES[val] = await linterMan.worker.getRules(val);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
function hint(cm) {
|
|
const rules = RULES[linter];
|
|
let ruleIds, options;
|
|
if (isStylelint) {
|
|
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 && isStylelint) {
|
|
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 onConfigCancel() {
|
|
helpPopup.close();
|
|
editor.closestVisible().focus();
|
|
}
|
|
|
|
function onConfigClose() {
|
|
cm = null;
|
|
}
|
|
|
|
function onConfigReset(event) {
|
|
event.preventDefault();
|
|
cm.setValue(defaultConfig);
|
|
cm.focus();
|
|
updateConfigButtons();
|
|
}
|
|
|
|
async function onConfigSave(event) {
|
|
if (event instanceof Event) {
|
|
event.preventDefault();
|
|
}
|
|
const json = tryJSONparse(cm.getValue());
|
|
if (!json) {
|
|
showLinterErrorMessage(linter, t('linterJSONError'), popup);
|
|
cm.focus();
|
|
return;
|
|
}
|
|
let invalid;
|
|
if (isStylelint) {
|
|
invalid = Object.keys(json.rules).filter(k => !RULES.stylelint.hasOwnProperty(k));
|
|
} else {
|
|
const ids = RULES.csslint.map(r => r.id);
|
|
invalid = Object.keys(json).filter(k => !ids.includes(k));
|
|
}
|
|
if (invalid.length) {
|
|
showLinterErrorMessage(linter, [
|
|
t('linterInvalidConfigError'),
|
|
$create('ul', invalid.map(name => $create('li', name))),
|
|
], popup);
|
|
return;
|
|
}
|
|
chromeSync.setLZValue(chromeSync.LZ_KEY[linter], json);
|
|
cm.markClean();
|
|
cm.focus();
|
|
updateConfigButtons();
|
|
}
|
|
|
|
function stringifyConfig(config) {
|
|
return JSON.stringify(config, null, 2)
|
|
.replace(/,\n\s+{\n\s+("severity":\s"\w+")\n\s+}/g, ', {$1}');
|
|
}
|
|
|
|
async function showLinterErrorMessage(title, contents, popup) {
|
|
await messageBoxProxy.show({
|
|
title,
|
|
contents,
|
|
className: 'danger center lint-config',
|
|
buttons: [t('confirmOK')],
|
|
});
|
|
if (popup && popup.codebox) {
|
|
popup.codebox.focus();
|
|
}
|
|
}
|
|
|
|
function updateConfigButtons() {
|
|
$('.save', popup).disabled = cm.isClean();
|
|
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
|
|
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
|
|
}
|
|
})();
|