| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- /* global CodeMirror messageBox */
- /* global editors makeSectionVisible showCodeMirrorPopup showHelp */
- /* global loadScript require CSSLint stylelint */
- /* global makeLink */
- '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',
- },
- getDefault() {
- // some dirty hacks to override editor.linter getting from prefs
- const linter = prefs.get('editor.linter');
- if (linter && editors[0] && editors[0].getOption('mode') !== 'css') {
- return 'stylelint';
- }
- return linter;
- },
- getCurrent(linter = linterConfig.getDefault()) {
- return this.fallbackToDefaults(this[linter] || {});
- },
- getForCodeMirror(linter = linterConfig.getDefault()) {
- return CodeMirror.lint && CodeMirror.lint[linter] ? {
- getAnnotations: CodeMirror.lint[linter],
- delay: prefs.get('editor.lintDelay'),
- } : false;
- },
- fallbackToDefaults(config, linter = linterConfig.getDefault()) {
- 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 config;
- } else {
- return deepCopy(this.defaults[linter] || {});
- }
- },
- setLinter(linter = linterConfig.getDefault()) {
- linter = linter.toLowerCase();
- linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
- if (linterConfig.getDefault() !== linter) {
- prefs.set('editor.linter', linter);
- }
- return linter;
- },
- findInvalidRules(config, linter = linterConfig.getDefault()) {
- const rules = linter === 'stylelint' ? config.rules : config;
- const allRules = new Set(
- linter === 'stylelint'
- ? Object.keys(stylelint.rules)
- : CSSLint.getRules().map(rule => rule.id)
- );
- return Object.keys(rules).filter(rule => !allRules.has(rule));
- },
- stringify(config = this.getCurrent()) {
- if (linterConfig.getDefault() === '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.getDefault();
- this[linter] = config;
- BG.chromeSync.setLZValue(this.storageName[linter], config);
- return config;
- },
- loadAll() {
- return BG.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() {
- setupLinterPopup(linterConfig.stringify());
- },
- showSavedMessage() {
- $('#help-popup .saved-message').classList.add('show');
- clearTimeout($('#help-popup .contents').timer);
- $('#help-popup .contents').timer = setTimeout(() => {
- // popup may be closed at this point
- const msg = $('#help-popup .saved-message');
- if (msg) {
- msg.classList.remove('show');
- }
- }, 2000);
- },
- init() {
- if (!linterConfig.init.pending) {
- linterConfig.init.pending = linterConfig.loadAll();
- }
- return linterConfig.init.pending;
- }
- };
- function initLint() {
- $('#lint-help').addEventListener('click', showLintHelp);
- $('#lint').addEventListener('click', gotoLintIssue);
- $('#linter-settings').addEventListener('click', linterConfig.openOnClick);
- window.addEventListener('resize', resizeLintReport);
- updateLinter();
- linterConfig.watchStorage();
- prefs.subscribe(['editor.linter'], updateLinter);
- }
- function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
- 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 => {
- 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($element({
- className: 'CodeMirror-gutter ' + GUTTERS_CLASS
- }));
- } else if (!linter && el) {
- el.remove();
- }
- }
- }
- function updateLintReport(cm, delay) {
- if (cm && !cm.options.lint) {
- // add 'lint' option back to the freshly created section
- setTimeout(() => {
- if (!cm.options.lint) {
- cm.setOption('lint', linterConfig.getForCodeMirror());
- }
- });
- }
- 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 ? {} : $element({
- tag: 'tbody',
- appendChild: 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 $element({
- tag: 'tr',
- className: info.severity,
- appendChild: [
- $element({
- tag: 'td',
- attributes: {role: 'severity'},
- dataset: {rule: info.rule},
- appendChild: $element({
- className: 'CodeMirror-lint-marker-' + info.severity,
- textContent: info.severity,
- }),
- }),
- $element({tag: 'td', attributes: {role: 'line'}, textContent: line + 1}),
- $element({tag: 'td', attributes: {role: 'sep'}, textContent: ':'}),
- $element({tag: 'td', attributes: {role: 'col'}, textContent: ch + 1}),
- $element({tag: 'td', attributes: {role: 'message'}, textContent: message, title}),
- ],
- });
- })
- });
- body.textContentCached = body.textContent || '';
- lintState.body = body.textContentCached && body;
- result.changed |= oldText !== body.textContentCached;
- result.fixedSome |= lintState.reportDisplayed && oldMarkers.size;
- return result;
- }
- function clipString(str, limit) {
- return str.length <= limit ? str : str.substr(0, limit) + '...';
- }
- }
- 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 = $element({
- tag: 'table',
- appendChild: [
- $element({tag: 'caption', textContent: label + ' ' + (index + 1)}),
- body,
- ],
- cm,
- });
- 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);
- resizeLintReport();
- }
- }
- function resizeLintReport() {
- // subtracted value to prevent scrollbar
- const magicBuffer = 20;
- const content = $('#lint table');
- if (content) {
- const bounds = content.getBoundingClientRect();
- const newMaxHeight = bounds.bottom <= window.innerHeight ? '' :
- // subtract out a bit of padding or the vertical scrollbar extends beyond the viewport
- (window.innerHeight - bounds.top - magicBuffer) + 'px';
- if (newMaxHeight !== content.style.maxHeight) {
- content.parentNode.style.maxHeight = newMaxHeight;
- }
- }
- }
- 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.getDefault();
- 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') {
- const CSSLintRules = CSSLint.getRules();
- headerLink = makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint');
- template = ruleID => {
- const rule = CSSLintRules.find(rule => rule.id === ruleID);
- return rule &&
- $element({tag: 'li', appendChild: [
- $element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}),
- $element({tag: 'br'}),
- rule.desc,
- ]});
- };
- } else {
- headerLink = makeLink(baseUrl, 'stylelint');
- template = rule =>
- $element({
- tag: 'li',
- appendChild: makeLink(baseUrl + rule, rule),
- });
- }
- const header = t('linterIssuesHelp', '\x01').split('\x01');
- const activeRules = new Set($$('#lint td[role="severity"]').map(el => el.dataset.rule));
- return showHelp(t('linterIssues'),
- $element({appendChild: [
- header[0], headerLink, header[1],
- $element({
- tag: 'ul',
- className: 'rules',
- appendChild: [...activeRules.values()].map(template),
- }),
- ]})
- );
- }
- function showLinterErrorMessage(title, contents) {
- messageBox({
- title,
- contents,
- className: 'danger center lint-config',
- buttons: [t('confirmOK')],
- });
- }
- function setupLinterSettingsEvents(popup) {
- $('.save', popup).addEventListener('click', event => {
- event.preventDefault();
- const linter = linterConfig.setLinter(event.target.dataset.linter);
- const json = tryJSONparse(popup.codebox.getValue());
- if (json) {
- const invalid = linterConfig.findInvalidRules(json, linter);
- if (invalid.length) {
- showLinterErrorMessage(linter, [
- t('linterInvalidConfigError'),
- $element({
- tag: 'ul',
- appendChild: invalid.map(name =>
- $element({tag: 'li', textContent: name})),
- }),
- ]);
- return;
- }
- linterConfig.save(json);
- linterConfig.showSavedMessage();
- popup.codebox.markClean();
- } else {
- showLinterErrorMessage(linter, t('linterJSONError'));
- }
- popup.codebox.focus();
- });
- $('.reset', popup).addEventListener('click', event => {
- event.preventDefault();
- const linter = linterConfig.setLinter(event.target.dataset.linter);
- popup.codebox.setValue(linterConfig.stringify(linterConfig.defaults[linter] || {}));
- popup.codebox.focus();
- });
- $('.cancel', popup).addEventListener('click', event => {
- event.preventDefault();
- $('.dismiss').dispatchEvent(new Event('click'));
- });
- }
- function setupLinterPopup(config) {
- const linter = linterConfig.getDefault();
- const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
- function makeButton(className, text, options = {}) {
- return $element(Object.assign(options, {
- tag: 'button',
- className,
- type: 'button',
- textContent: t(text),
- dataset: {linter}
- }));
- }
- function makeLink(url, textContent) {
- return $element({tag: 'a', target: '_blank', href: url, textContent});
- }
- const title = t('linterConfigPopupTitle', linterTitle);
- const contents = $element({
- appendChild: [
- $element({
- tag: 'p',
- appendChild: [
- t('linterRulesLink') + ' ',
- makeLink(
- linter === 'stylelint'
- ? 'https://stylelint.io/user-guide/rules/'
- : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
- linterTitle
- ),
- linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : ''
- ]
- }),
- makeButton('save', 'styleSaveLabel', {disabled: true}),
- makeButton('cancel', 'confirmCancel'),
- makeButton('reset', 'genericResetLabel', {title: t('linterResetMessage')}),
- $element({
- tag: 'span',
- className: 'saved-message',
- textContent: t('genericSavedMessage')
- })
- ]
- });
- const popup = showCodeMirrorPopup(title, contents, {lint: false});
- contents.parentNode.appendChild(contents);
- popup.codebox.focus();
- popup.codebox.setValue(config);
- popup.codebox.clearHistory();
- popup.codebox.markClean();
- popup.codebox.on('change', cm => {
- $('.save', popup).disabled = cm.isClean();
- });
- setupLinterSettingsEvents(popup);
- loadScript([
- '/vendor/codemirror/mode/javascript/javascript.js',
- '/vendor/codemirror/addon/lint/json-lint.js',
- '/vendor/jsonlint/jsonlint.js'
- ]).then(() => {
- popup.codebox.setOption('mode', 'application/json');
- popup.codebox.setOption('lint', 'json');
- });
- }
- function loadLinterAssets(name = linterConfig.getDefault()) {
- if (!name) {
- return Promise.resolve();
- }
- return loadLibrary().then(loadAddon);
- function loadLibrary() {
- if (name === 'csslint' && !window.CSSLint) {
- return loadScript([
- '/vendor-overwrites/csslint/csslint-worker.js',
- '/edit/lint-defaults-csslint.js'
- ]);
- }
- if (name === 'stylelint' && !window.stylelint) {
- return loadScript([
- '/vendor-overwrites/stylelint/stylelint-bundle.min.js',
- '/edit/lint-defaults-stylelint.js'
- ]).then(() => (window.stylelint = require('stylelint')));
- }
- return Promise.resolve();
- }
- function loadAddon() {
- if (CodeMirror.lint) {
- return;
- }
- return loadScript([
- '/vendor/codemirror/addon/lint/lint.css',
- '/msgbox/msgbox.css',
- '/vendor/codemirror/addon/lint/lint.js',
- '/edit/lint-codemirror-helper.js',
- '/msgbox/msgbox.js'
- ]);
- }
- }
|