| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- /* 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');
- }
- })();
|