linter-dialogs.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /* global $ $create $createLink messageBoxProxy */// dom.js
  2. /* global chromeSync */// storage-util.js
  3. /* global editor */
  4. /* global helpPopup showCodeMirrorPopup */// util.js
  5. /* global linterMan */
  6. /* global t */// localization.js
  7. /* global tryJSONparse */// toolbox.js
  8. 'use strict';
  9. (() => {
  10. /** @type {{csslint:{}, stylelint:{}}} */
  11. const RULES = {};
  12. let cm;
  13. let defaultConfig;
  14. let isStylelint;
  15. let linter;
  16. let popup;
  17. linterMan.showLintConfig = async () => {
  18. linter = await getLinter();
  19. if (!linter) {
  20. return;
  21. }
  22. await require([
  23. '/vendor/codemirror/mode/javascript/javascript',
  24. '/vendor/codemirror/addon/lint/json-lint',
  25. '/vendor/jsonlint/jsonlint',
  26. ]);
  27. const config = await chromeSync.getLZValue(chromeSync.LZ_KEY[linter]);
  28. const title = t('linterConfigPopupTitle', isStylelint ? 'Stylelint' : 'CSSLint');
  29. isStylelint = linter === 'stylelint';
  30. defaultConfig = stringifyConfig(linterMan.DEFAULTS[linter]);
  31. popup = showCodeMirrorPopup(title, null, {
  32. extraKeys: {'Ctrl-Enter': onConfigSave},
  33. hintOptions: {hint},
  34. lint: true,
  35. mode: 'application/json',
  36. value: config ? stringifyConfig(config) : defaultConfig,
  37. });
  38. $('.contents', popup).appendChild(
  39. $create('div', [
  40. $create('p', [
  41. $createLink(
  42. isStylelint
  43. ? 'https://stylelint.io/user-guide/rules/'
  44. : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
  45. t('linterRulesLink')),
  46. linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '',
  47. ]),
  48. $create('.buttons', [
  49. $create('button.save', {onclick: onConfigSave, title: 'Ctrl-Enter'},
  50. t('styleSaveLabel')),
  51. $create('button.cancel', {onclick: onConfigCancel}, t('confirmClose')),
  52. $create('button.reset', {onclick: onConfigReset, title: t('linterResetMessage')},
  53. t('genericResetLabel')),
  54. ]),
  55. ]));
  56. cm = popup.codebox;
  57. cm.focus();
  58. const rulesStr = getActiveRules().join('|');
  59. if (rulesStr) {
  60. const rx = new RegExp(`"(${rulesStr})"\\s*:`);
  61. let line = 0;
  62. cm.startOperation();
  63. cm.eachLine(({text}) => {
  64. const m = rx.exec(text);
  65. if (m) {
  66. const ch = m.index + 1;
  67. cm.markText({line, ch}, {line, ch: ch + m[1].length}, {className: 'active-linter-rule'});
  68. }
  69. ++line;
  70. });
  71. cm.endOperation();
  72. }
  73. cm.on('changes', updateConfigButtons);
  74. updateConfigButtons();
  75. window.on('closeHelp', onConfigClose, {once: true});
  76. };
  77. linterMan.showLintHelp = async () => {
  78. const linter = await getLinter();
  79. const baseUrl = linter === 'stylelint'
  80. ? 'https://stylelint.io/user-guide/rules/'
  81. : '';
  82. let headerLink, template;
  83. if (linter === 'csslint') {
  84. headerLink = $createLink('https://github.com/CSSLint/csslint/wiki/Rules', 'CSSLint');
  85. template = ruleID => {
  86. const rule = RULES.csslint.find(rule => rule.id === ruleID);
  87. return rule &&
  88. $create('li', [
  89. $create('b', ruleID + ': '),
  90. rule.url ? $createLink(rule.url, rule.name) : $create('span', `"${rule.name}"`),
  91. $create('p', rule.desc),
  92. ]);
  93. };
  94. } else {
  95. headerLink = $createLink(baseUrl, 'stylelint');
  96. template = rule =>
  97. $create('li',
  98. rule === 'CssSyntaxError' ? rule : $createLink(baseUrl + rule, rule));
  99. }
  100. const header = t('linterIssuesHelp', '\x01').split('\x01');
  101. helpPopup.show(t('linterIssues'),
  102. $create([
  103. header[0], headerLink, header[1],
  104. $create('ul.rules', getActiveRules().map(template)),
  105. $create('button', {onclick: linterMan.showLintConfig}, t('configureStyle')),
  106. ]));
  107. };
  108. function getActiveRules() {
  109. const all = [...linterMan.getIssues()].map(issue => issue.rule);
  110. const uniq = new Set(all);
  111. return [...uniq];
  112. }
  113. function getLexicalDepth(lexicalState) {
  114. let depth = 0;
  115. while ((lexicalState = lexicalState.prev)) {
  116. depth++;
  117. }
  118. return depth;
  119. }
  120. async function getLinter() {
  121. const val = $('#editor.linter').value;
  122. if (val && !RULES[val]) {
  123. RULES[val] = await linterMan.worker.getRules(val);
  124. }
  125. return val;
  126. }
  127. function hint(cm) {
  128. const rules = RULES[linter];
  129. let ruleIds, options;
  130. if (isStylelint) {
  131. ruleIds = Object.keys(rules);
  132. options = rules;
  133. } else {
  134. ruleIds = rules.map(r => r.id);
  135. options = {};
  136. }
  137. const cursor = cm.getCursor();
  138. const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
  139. const {line, ch} = cursor;
  140. const quoted = string.startsWith('"');
  141. const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
  142. const depth = getLexicalDepth(lexical);
  143. const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
  144. let [, prevWord] = search.find(true) || [];
  145. let words = [];
  146. if (depth === 1 && isStylelint) {
  147. words = quoted ? ['rules'] : [];
  148. } else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
  149. words = ruleIds;
  150. } else if (depth === 2 || depth === 3 && lexical.type === ']') {
  151. words = !quoted ? ['true', 'false', 'null'] :
  152. ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
  153. } else if (depth === 4 && prevWord === 'severity') {
  154. words = ['error', 'warning'];
  155. } else if (depth === 4) {
  156. words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
  157. } else if (depth === 5 && lexical.type === ']' && quoted) {
  158. while (prevWord && !ruleIds.includes(prevWord)) {
  159. prevWord = (search.find(true) || [])[1];
  160. }
  161. words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
  162. }
  163. return {
  164. list: words.filter(word => word.startsWith(leftPart)),
  165. from: {line, ch: start + (quoted ? 1 : 0)},
  166. to: {line, ch: string.endsWith('"') ? end - 1 : end},
  167. };
  168. }
  169. function onConfigCancel() {
  170. helpPopup.close();
  171. editor.closestVisible().focus();
  172. }
  173. function onConfigClose() {
  174. cm = null;
  175. }
  176. function onConfigReset(event) {
  177. event.preventDefault();
  178. cm.setValue(defaultConfig);
  179. cm.focus();
  180. updateConfigButtons();
  181. }
  182. async function onConfigSave(event) {
  183. if (event instanceof Event) {
  184. event.preventDefault();
  185. }
  186. const json = tryJSONparse(cm.getValue());
  187. if (!json) {
  188. showLinterErrorMessage(linter, t('linterJSONError'), popup);
  189. cm.focus();
  190. return;
  191. }
  192. let invalid;
  193. if (isStylelint) {
  194. invalid = Object.keys(json.rules).filter(k => !RULES.stylelint.hasOwnProperty(k));
  195. } else {
  196. const ids = RULES.csslint.map(r => r.id);
  197. invalid = Object.keys(json).filter(k => !ids.includes(k));
  198. }
  199. if (invalid.length) {
  200. showLinterErrorMessage(linter, [
  201. t('linterInvalidConfigError'),
  202. $create('ul', invalid.map(name => $create('li', name))),
  203. ], popup);
  204. return;
  205. }
  206. chromeSync.setLZValue(chromeSync.LZ_KEY[linter], json);
  207. cm.markClean();
  208. cm.focus();
  209. updateConfigButtons();
  210. }
  211. function stringifyConfig(config) {
  212. return JSON.stringify(config, null, 2)
  213. .replace(/,\n\s+{\n\s+("severity":\s"\w+")\n\s+}/g, ', {$1}');
  214. }
  215. async function showLinterErrorMessage(title, contents, popup) {
  216. await messageBoxProxy.show({
  217. title,
  218. contents,
  219. className: 'danger center lint-config',
  220. buttons: [t('confirmOK')],
  221. });
  222. if (popup && popup.codebox) {
  223. popup.codebox.focus();
  224. }
  225. }
  226. function updateConfigButtons() {
  227. $('.save', popup).disabled = cm.isClean();
  228. $('.reset', popup).disabled = cm.getValue() === defaultConfig;
  229. $('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
  230. }
  231. })();